Compare commits
8 Commits
0.1.0
...
9d28175b17
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d28175b17 | |||
| ac4d56764c | |||
| 69fe607157 | |||
| 92e896b973 | |||
| dd7c997816 | |||
| ad3e73fdd8 | |||
| 177809fe1a | |||
| 69f655d179 |
@@ -1,5 +1,6 @@
|
|||||||
[[building]]
|
[[building]]
|
||||||
id = "belt"
|
id = "belt"
|
||||||
|
tooltip = "Transports items one tile at a time in the direction it faces."
|
||||||
cost = 2
|
cost = 2
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.2
|
construction_time_seconds = 0.2
|
||||||
@@ -7,6 +8,7 @@ surface_mask = ["A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "splitter"
|
id = "splitter"
|
||||||
|
tooltip = "Splits an incoming item stream between two outputs, with optional per-output filters."
|
||||||
cost = 3
|
cost = 3
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.5
|
construction_time_seconds = 0.5
|
||||||
@@ -14,6 +16,7 @@ surface_mask = ["<A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "tunnel_entry"
|
id = "tunnel_entry"
|
||||||
|
tooltip = "Sends items underground to a matching tunnel exit, letting belts cross."
|
||||||
cost = 5
|
cost = 5
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.5
|
construction_time_seconds = 0.5
|
||||||
@@ -21,6 +24,7 @@ surface_mask = ["A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "tunnel_exit"
|
id = "tunnel_exit"
|
||||||
|
tooltip = "Receives items from a matching tunnel entry and pushes them onward."
|
||||||
cost = 5
|
cost = 5
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.5
|
construction_time_seconds = 0.5
|
||||||
@@ -28,6 +32,7 @@ surface_mask = ["A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "miner"
|
id = "miner"
|
||||||
|
tooltip = "Extracts a selected ore from the asteroid; every tile yields any ore."
|
||||||
cost = 15
|
cost = 15
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -37,6 +42,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "smelter"
|
id = "smelter"
|
||||||
|
tooltip = "Melts ore or scrap into basic materials. No recipe selection needed."
|
||||||
cost = 20
|
cost = 20
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -47,6 +53,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "assembler"
|
id = "assembler"
|
||||||
|
tooltip = "Crafts a selected recipe from the production tree into intermediate or final parts."
|
||||||
cost = 35
|
cost = 35
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -57,6 +64,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "reprocessing_plant"
|
id = "reprocessing_plant"
|
||||||
|
tooltip = "Consumes scrap and yields one random higher-tier product per cycle."
|
||||||
cost = 40
|
cost = 40
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -68,6 +76,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "shipyard"
|
id = "shipyard"
|
||||||
|
tooltip = "Builds autonomous combat ships from a selected schematic and module layout."
|
||||||
cost = 60
|
cost = 60
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -78,6 +87,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "salvage_bay"
|
id = "salvage_bay"
|
||||||
|
tooltip = "Drop-off point where salvage ships unload collected scrap onto belts."
|
||||||
cost = 25
|
cost = 25
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_s"
|
id = "railgun_s"
|
||||||
|
tooltip = "Small railgun. Fast-firing, short range, low damage; fits any hull."
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "railgun_s_module", amount = 1}]
|
materials = [{item = "railgun_s_module", amount = 1}]
|
||||||
@@ -47,6 +48,7 @@ attack_rate_hz = 2.0
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_m"
|
id = "railgun_m"
|
||||||
|
tooltip = "Medium railgun. Higher damage at longer range; needs a 2x2 slot."
|
||||||
unlock_at_station_level = 2
|
unlock_at_station_level = 2
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
@@ -64,6 +66,7 @@ attack_rate_hz = 1.5
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_l"
|
id = "railgun_l"
|
||||||
|
tooltip = "Large railgun. Heavy damage at long range; needs a 3x3 slot."
|
||||||
unlock_at_station_level = 6
|
unlock_at_station_level = 6
|
||||||
unlock_requires = ["railgun_m"]
|
unlock_requires = ["railgun_m"]
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
@@ -86,6 +89,7 @@ attack_rate_hz = 0.8
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "salvager"
|
id = "salvager"
|
||||||
|
tooltip = "Collects scrap from wrecks and stores it in the ship's cargo hold."
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "salvager_module", amount = 1}]
|
materials = [{item = "salvager_module", amount = 1}]
|
||||||
@@ -101,6 +105,7 @@ collection_rate_hz = 0.5
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "repair_tool"
|
id = "repair_tool"
|
||||||
|
tooltip = "Repairs damaged friendly ships and defence stations within range."
|
||||||
unlock_at_station_level = 0
|
unlock_at_station_level = 0
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "repair_tool_module", amount = 1}]
|
materials = [{item = "repair_tool_module", amount = 1}]
|
||||||
@@ -119,6 +124,7 @@ repair_range_m = 80
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "afterburner"
|
id = "afterburner"
|
||||||
|
tooltip = "Greatly boosts top speed and forward acceleration."
|
||||||
unlock_at_station_level = 2
|
unlock_at_station_level = 2
|
||||||
surface_mask = ["OOO"]
|
surface_mask = ["OOO"]
|
||||||
materials = [{item = "afterburner_module", amount = 1}]
|
materials = [{item = "afterburner_module", amount = 1}]
|
||||||
@@ -133,6 +139,7 @@ added_main_acceleration_mpss = 60
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "maneuvering_thrusters"
|
id = "maneuvering_thrusters"
|
||||||
|
tooltip = "Improves top speed and lateral/braking acceleration."
|
||||||
unlock_at_station_level = 1
|
unlock_at_station_level = 1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||||
@@ -150,6 +157,7 @@ added_maneuvering_acceleration_mpss = 10
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "armor_plates"
|
id = "armor_plates"
|
||||||
|
tooltip = "Adds a large flat bonus to the ship's hit points."
|
||||||
unlock_at_station_level = 0
|
unlock_at_station_level = 0
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "armor_plates_module", amount = 1}]
|
materials = [{item = "armor_plates_module", amount = 1}]
|
||||||
@@ -163,6 +171,7 @@ added_hp = 1200
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "sensor_booster"
|
id = "sensor_booster"
|
||||||
|
tooltip = "Extends the ship's sensor range."
|
||||||
unlock_at_station_level = 1
|
unlock_at_station_level = 1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
@@ -179,6 +188,7 @@ added_sensor_range_m = 50
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_upgrade"
|
id = "weapon_upgrade"
|
||||||
|
tooltip = "Increases the damage of all weapons on the ship."
|
||||||
unlock_at_station_level = 4
|
unlock_at_station_level = 4
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
@@ -195,6 +205,7 @@ multiplied_damage = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_primer"
|
id = "weapon_primer"
|
||||||
|
tooltip = "Increases the fire rate of all weapons on the ship."
|
||||||
unlock_at_station_level = 4
|
unlock_at_station_level = 4
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
@@ -211,6 +222,7 @@ multiplied_attack_rate_hz = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_stabilizer"
|
id = "weapon_stabilizer"
|
||||||
|
tooltip = "Extends weapon range at the cost of some fire rate."
|
||||||
unlock_at_station_level = 3
|
unlock_at_station_level = 3
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
@@ -234,6 +246,7 @@ multiplied_attack_rate_hz = 0.8
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "drone_bay"
|
id = "drone_bay"
|
||||||
|
tooltip = "Drone launch bay (capability not yet implemented)."
|
||||||
unlock_at_station_level = 5
|
unlock_at_station_level = 5
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
@@ -246,6 +259,7 @@ glyph = "Db"
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "drone_hangar"
|
id = "drone_hangar"
|
||||||
|
tooltip = "Large drone hangar (capability not yet implemented)."
|
||||||
unlock_at_station_level = 9
|
unlock_at_station_level = 9
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ tunnel_max_distance_tiles = 10
|
|||||||
departure_interval_seconds = 20
|
departure_interval_seconds = 20
|
||||||
orbit_factor = 0.8
|
orbit_factor = 0.8
|
||||||
rally_orbit_radius_tiles = 5.0
|
rally_orbit_radius_tiles = 5.0
|
||||||
|
building_blocks_tooltip = "Building blocks are the currency for construction. Spend them to place buildings and to expand the asteroid. Produce building blocks in your factory and deliver them to the HQ on a belt to grow your stock."
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width_tiles = 60
|
asteroid_width_tiles = 60
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ cost = 25
|
|||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 15
|
construction_time_seconds = 15
|
||||||
output_buffer_capacity = 20
|
output_buffer_capacity = 20
|
||||||
|
tooltip = "Drop-off point for salvage ships."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"SAA",
|
"SAA",
|
||||||
"SAA>",
|
"SAA>",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
[[module]]
|
[[module]]
|
||||||
id = "armor_plate"
|
id = "armor_plate"
|
||||||
|
tooltip = "Adds a large flat bonus to hit points."
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ tunnel_max_distance_tiles = 10
|
|||||||
departure_interval_seconds = 20
|
departure_interval_seconds = 20
|
||||||
orbit_factor = 0.8
|
orbit_factor = 0.8
|
||||||
rally_orbit_radius_tiles = 5.0
|
rally_orbit_radius_tiles = 5.0
|
||||||
|
building_blocks_tooltip = "Spend building blocks to build; deliver them to the HQ to gain more."
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width_tiles = 40
|
asteroid_width_tiles = 40
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
Config files use the TOML format. The following config files drive game parameters:
|
Config files use the TOML format. The following config files drive game parameters:
|
||||||
|
|
||||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, and view pan speeds (slow and fast horizontal pan speed and pan ramp band width).
|
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), and an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset).
|
||||||
- **buildings.toml** — building block cost and construction time per building type.
|
- **buildings.toml** — building block cost and construction time per building type, plus an optional tooltip description string per building type (shown as the build button's hover tooltip, REQ-UI-BUILD-TOOLTIP; omitted when unset).
|
||||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). An assembler recipe schematic entry may also define an optional `unlock_requires` list of prerequisite schematic ids (REQ-LOCK-PREREQ).
|
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). An assembler recipe schematic entry may also define an optional `unlock_requires` list of prerequisite schematic ids (REQ-LOCK-PREREQ).
|
||||||
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
||||||
- **modules.toml** — per module type: id, surface mask, materials list, production time, fill color, glyph, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), and an optional capability section and/or stat modifier formulas. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
|
- **modules.toml** — per module type: id, surface mask, materials list, production time, fill color, glyph, an optional tooltip description string (shown as the module selection button's hover tooltip, REQ-MOD-UI-MODULE-TOOLTIP; omitted when unset), the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), and an optional capability section and/or stat modifier formulas. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
|
||||||
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
||||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
||||||
- **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it.
|
- **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it.
|
||||||
@@ -259,7 +259,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
### Module UI
|
### Module UI
|
||||||
|
|
||||||
- REQ-MOD-UI-PREVIEW: When a schematic is selected in a shipyard's selected building panel, a small non-interactive **ship layout preview** widget is shown below the schematic selection button (REQ-UI-SELECT-BUTTON). The preview renders the ship's layout grid at a reduced scale: buildable cells without a module are shown as white, non-buildable cells are shown as black, and cells occupied by a module are shown in that module's `fill_color` with the module's `glyph` character. Below the preview, a "Configure" button is shown.
|
- REQ-MOD-UI-PREVIEW: For a selected shipyard (operational building or construction site), the selected building panel always shows a small non-interactive **ship layout preview** widget below the schematic selection button (REQ-UI-SELECT-BUTTON) and a "Configure" button below the preview. Both are **disabled while no schematic is selected**, and enabled once one is; the preview then shows an empty placeholder in place of a layout grid. When a schematic is selected, the preview renders the ship's layout grid at a reduced scale: buildable cells without a module are shown as white, non-buildable cells are shown as black, and cells occupied by a module are shown in that module's `fill_color` with the module's `glyph` character. For non-shipyard buildings, neither the preview nor the "Configure" button is shown.
|
||||||
- REQ-MOD-UI-DIALOG: Clicking the "Configure" button opens the **layout configuration dialog** as a modal. While the dialog is open, the game is paused (speed set to 0×). On close, the game speed is restored to what it was before the dialog was opened.
|
- REQ-MOD-UI-DIALOG: Clicking the "Configure" button opens the **layout configuration dialog** as a modal. While the dialog is open, the game is paused (speed set to 0×). On close, the game speed is restored to what it was before the dialog was opened.
|
||||||
|
|
||||||
The dialog contains:
|
The dialog contains:
|
||||||
@@ -269,6 +269,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
||||||
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
||||||
|
|
||||||
|
- REQ-MOD-UI-MODULE-TOOLTIP: Each module selection button in the layout configuration dialog (REQ-MOD-UI-DIALOG) shows a hover tooltip with the descriptive text defined for that module type in `modules.toml` (the optional per-module tooltip field). If a module type defines no tooltip text, its button shows no tooltip. The "Remove" button is not a module type and has no config-defined tooltip.
|
||||||
|
|
||||||
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
||||||
|
|
||||||
The panel always shows all hull stats as final computed values:
|
The panel always shows all hull stats as final computed values:
|
||||||
@@ -401,6 +403,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
```
|
```
|
||||||
|
|
||||||
- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) and an asteroid expansion button (REQ-UI-EXPAND-BUTTON) to the left of the speed buttons, and game speed controls on the right.
|
- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) and an asteroid expansion button (REQ-UI-EXPAND-BUTTON) to the left of the speed buttons, and game speed controls on the right.
|
||||||
|
- REQ-UI-BLOCKS-TOOLTIP: The header bar's building blocks stock display (REQ-UI-HEADER) shows a hover tooltip with the descriptive text defined in `world.toml [world].building_blocks_tooltip` — intended to tell the player what building blocks are used for and how to obtain them. If the field is unset, the stock display shows no tooltip. This tooltip is distinct from the build/module button tooltips (REQ-UI-BUILD-TOOLTIP, REQ-MOD-UI-MODULE-TOOLTIP).
|
||||||
- REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
|
- REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
|
||||||
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
|
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
|
||||||
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x> Blocks`, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x> Blocks`, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
||||||
@@ -458,7 +461,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
||||||
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
||||||
- REQ-UI-MULTI-SELECT: The player selects multiple buildings by box-drag or by Ctrl+clicking individual buildings to add or remove them from the selection.
|
- REQ-UI-MULTI-SELECT: The player selects multiple buildings by box-drag or by Ctrl+clicking individual buildings to add or remove them from the selection.
|
||||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown.
|
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button grid); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-BUTTON). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||||
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
||||||
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
||||||
- REQ-UI-SELECT-TOOLTIP: **Selection info tooltip.** Hovering an option button in the selection dialog (REQ-UI-SELECT-BUTTON), and hovering the selection button in the selected building panel when a selection is set, displays an info tooltip:
|
- REQ-UI-SELECT-TOOLTIP: **Selection info tooltip.** Hovering an option button in the selection dialog (REQ-UI-SELECT-BUTTON), and hovering the selection button in the selected building panel when a selection is set, displays an info tooltip:
|
||||||
@@ -473,6 +476,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
|
|
||||||
- REQ-UI-BUILD-GRID: All placeable building types are shown as a flat grid of buttons with no grouping.
|
- REQ-UI-BUILD-GRID: All placeable building types are shown as a flat grid of buttons with no grouping.
|
||||||
- REQ-UI-BUILD-COST: Each button caption shows the building name and its building block cost, e.g. "Belt: 2 Blocks".
|
- REQ-UI-BUILD-COST: Each button caption shows the building name and its building block cost, e.g. "Belt: 2 Blocks".
|
||||||
|
- REQ-UI-BUILD-TOOLTIP: Each building-type button shows a hover tooltip with the descriptive text defined for that building type in `buildings.toml` (the optional per-building tooltip field). This tooltip is distinct from the recipe/schematic selection tooltip (REQ-UI-SELECT-TOOLTIP). If a building type defines no tooltip text, its button shows no tooltip. The Demolish button (REQ-UI-DEMOLISH-BUTTON) is not a building type and has no config-defined tooltip.
|
||||||
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled.
|
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled.
|
||||||
- REQ-UI-DEMOLISH-BUTTON: A dedicated **Demolish** button is shown in the build button grid. Clicking it toggles demolish mode on and off, equivalent to the Q demolish toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while demolish mode is active.
|
- REQ-UI-DEMOLISH-BUTTON: A dedicated **Demolish** button is shown in the build button grid. Clicking it toggles demolish mode on and off, equivalent to the Q demolish toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while demolish mode is active.
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,26 @@ struct BuildingDef
|
|||||||
// Output-buffer holding size for buildings without a recipe-driven buffer.
|
// Output-buffer holding size for buildings without a recipe-driven buffer.
|
||||||
// Only the Salvage Bay sets this (REQ-BLD-SALVAGE-BAY).
|
// Only the Salvage Bay sets this (REQ-BLD-SALVAGE-BAY).
|
||||||
std::optional<int> outputBufferCapacity;
|
std::optional<int> outputBufferCapacity;
|
||||||
|
|
||||||
|
// Optional hover-tooltip text for the build button (REQ-UI-BUILD-TOOLTIP).
|
||||||
|
std::optional<std::string> tooltip;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BuildingsConfig
|
struct BuildingsConfig
|
||||||
{
|
{
|
||||||
std::vector<BuildingDef> buildings;
|
std::vector<BuildingDef> buildings;
|
||||||
|
|
||||||
|
// Returns the definition for the given building type, or nullptr if the
|
||||||
|
// type has no entry in buildings.toml.
|
||||||
|
const BuildingDef* findBuildingDef(BuildingType type) const
|
||||||
|
{
|
||||||
|
for (const BuildingDef& def : buildings)
|
||||||
|
{
|
||||||
|
if (def.type == type)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -273,6 +273,12 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
||||||
cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
|
cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
|
||||||
|
|
||||||
|
if (const std::optional<std::string> tip =
|
||||||
|
tbl["world"]["building_blocks_tooltip"].value<std::string>())
|
||||||
|
{
|
||||||
|
cfg.buildingBlocksTooltip = *tip;
|
||||||
|
}
|
||||||
|
|
||||||
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
||||||
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
||||||
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
||||||
@@ -343,6 +349,11 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
|||||||
requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
|
requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mt.contains("tooltip"))
|
||||||
|
{
|
||||||
|
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||||
|
}
|
||||||
|
|
||||||
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||||
if (!parsedType)
|
if (!parsedType)
|
||||||
{
|
{
|
||||||
@@ -603,6 +614,11 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||||
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||||
|
|
||||||
|
if (mt.contains("tooltip"))
|
||||||
|
{
|
||||||
|
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||||
|
}
|
||||||
|
|
||||||
// Materials
|
// Materials
|
||||||
{
|
{
|
||||||
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials");
|
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials");
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ struct ModuleDef
|
|||||||
std::optional<ModuleWeaponCapability> weaponCapability;
|
std::optional<ModuleWeaponCapability> weaponCapability;
|
||||||
std::optional<ModuleSalvageCapability> salvageCapability;
|
std::optional<ModuleSalvageCapability> salvageCapability;
|
||||||
std::optional<ModuleRepairCapability> repairCapability;
|
std::optional<ModuleRepairCapability> repairCapability;
|
||||||
|
|
||||||
|
// Optional hover-tooltip text for the module selection button
|
||||||
|
// (REQ-MOD-UI-MODULE-TOOLTIP).
|
||||||
|
std::optional<std::string> tooltip;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModulesConfig
|
struct ModulesConfig
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "Formula.h"
|
#include "Formula.h"
|
||||||
|
|
||||||
// Region widths are in tiles (REQ-GW-REGIONS).
|
// Region widths are in tiles (REQ-GW-REGIONS).
|
||||||
@@ -75,6 +78,10 @@ struct WorldConfig
|
|||||||
double orbitFactor; // REQ-SHP-ORBIT (multiplies tool range for orbit radius)
|
double orbitFactor; // REQ-SHP-ORBIT (multiplies tool range for orbit radius)
|
||||||
double rallyOrbitRadius_tiles; // REQ-SHP-ORBIT (fixed orbit radius around the rally point)
|
double rallyOrbitRadius_tiles; // REQ-SHP-ORBIT (fixed orbit radius around the rally point)
|
||||||
|
|
||||||
|
// Optional hover-tooltip for the header building blocks stock display
|
||||||
|
// (REQ-UI-BLOCKS-TOOLTIP). Presentation-only; the simulation ignores it.
|
||||||
|
std::optional<std::string> buildingBlocksTooltip;
|
||||||
|
|
||||||
WorldRegions regions;
|
WorldRegions regions;
|
||||||
WorldExpansion expansion;
|
WorldExpansion expansion;
|
||||||
WorldPush push;
|
WorldPush push;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <random>
|
#include <random>
|
||||||
@@ -9,6 +10,18 @@
|
|||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Smelter and Reprocessing Plant have no player-selected recipe
|
||||||
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They auto-process whatever inputs
|
||||||
|
// they receive, matching against every recipe of their building type.
|
||||||
|
bool isAutoRecipeBuildingType(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Smelter
|
||||||
|
|| type == BuildingType::ReprocessingPlant;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||||
BeltSystem& belts,
|
BeltSystem& belts,
|
||||||
std::function<BuildingId()> allocateBuildingId,
|
std::function<BuildingId()> allocateBuildingId,
|
||||||
@@ -118,6 +131,56 @@ void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildingSystem::initAutoBuffers(Building& b) const
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts.clear();
|
||||||
|
b.inputBuffer.caps.clear();
|
||||||
|
|
||||||
|
// Union the inputs of every recipe of this building type; the cap for each
|
||||||
|
// item is twice the largest per-cycle requirement across those recipes.
|
||||||
|
// Output capacity follows the same rules as initBuffers: the Reprocessing
|
||||||
|
// Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
|
||||||
|
// other auto buildings hold twice the largest per-cycle output.
|
||||||
|
int outputCapacity = 0;
|
||||||
|
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building != b.type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts[type] = 0;
|
||||||
|
b.inputBuffer.caps[type] =
|
||||||
|
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.type == BuildingType::ReprocessingPlant)
|
||||||
|
{
|
||||||
|
int maxAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
maxAmount = std::max(maxAmount, out.amount);
|
||||||
|
}
|
||||||
|
outputCapacity = std::max(outputCapacity, maxAmount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int totalAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
totalAmount += out.amount;
|
||||||
|
}
|
||||||
|
outputCapacity = std::max(outputCapacity, 2 * totalAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
b.outputBuffer.capacity = outputCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
void BuildingSystem::initShipyardBuffers(Building& b) const
|
void BuildingSystem::initShipyardBuffers(Building& b) const
|
||||||
{
|
{
|
||||||
b.inputBuffer.counts.clear();
|
b.inputBuffer.counts.clear();
|
||||||
@@ -419,6 +482,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
|||||||
{
|
{
|
||||||
if (site.id == id)
|
if (site.id == id)
|
||||||
{
|
{
|
||||||
|
// Auto-recipe buildings have no player-selected recipe
|
||||||
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
|
||||||
|
if (isAutoRecipeBuildingType(site.type))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
// No-op if the recipe is unchanged, so a redundant selection does
|
// No-op if the recipe is unchanged, so a redundant selection does
|
||||||
// not wipe an already-configured ship layout.
|
// not wipe an already-configured ship layout.
|
||||||
if (site.recipeId == recipeId)
|
if (site.recipeId == recipeId)
|
||||||
@@ -436,6 +505,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
|||||||
{
|
{
|
||||||
if (building.id == id)
|
if (building.id == id)
|
||||||
{
|
{
|
||||||
|
// Auto-recipe buildings have no player-selected recipe
|
||||||
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
|
||||||
|
if (isAutoRecipeBuildingType(building.type))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
// No-op if the recipe is unchanged, so a redundant selection does
|
// No-op if the recipe is unchanged, so a redundant selection does
|
||||||
// not wipe an already-configured ship layout or reset buffers.
|
// not wipe an already-configured ship layout or reset buffers.
|
||||||
if (building.recipeId == recipeId)
|
if (building.recipeId == recipeId)
|
||||||
@@ -603,6 +678,12 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
{
|
{
|
||||||
initSalvageBayBuffer(building);
|
initSalvageBayBuffer(building);
|
||||||
}
|
}
|
||||||
|
else if (isAutoRecipeBuildingType(building.type))
|
||||||
|
{
|
||||||
|
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
|
||||||
|
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
|
initAutoBuffers(building);
|
||||||
|
}
|
||||||
else if (!building.recipeId.empty())
|
else if (!building.recipeId.empty())
|
||||||
{
|
{
|
||||||
if (building.type == BuildingType::Shipyard)
|
if (building.type == BuildingType::Shipyard)
|
||||||
@@ -684,6 +765,11 @@ void BuildingSystem::tickBeltPull()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-recipe buildings (Smelter, Reprocessing Plant) accept any item
|
||||||
|
// that is an input to one of their recipes; their caps already span the
|
||||||
|
// union of those inputs (initAutoBuffers), so no recipe lookup is needed.
|
||||||
|
if (!isAutoRecipeBuildingType(building.type))
|
||||||
|
{
|
||||||
if (building.recipeId.empty())
|
if (building.recipeId.empty())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -697,6 +783,7 @@ void BuildingSystem::tickBeltPull()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const Port& port : building.inputPorts)
|
for (const Port& port : building.inputPorts)
|
||||||
{
|
{
|
||||||
@@ -752,18 +839,15 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (building.recipeId.empty())
|
const bool autoRecipe = isAutoRecipeBuildingType(building.type);
|
||||||
|
if (!autoRecipe && building.recipeId.empty())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
// If a production cycle is active, check for completion. Completion only
|
||||||
if (!recipe)
|
// needs the already-decided outputs, so it does not depend on which
|
||||||
{
|
// recipe is selected or auto-chosen.
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a production cycle is active, check for completion.
|
|
||||||
if (building.production)
|
if (building.production)
|
||||||
{
|
{
|
||||||
if (currentTick >= building.production->completesAt)
|
if (currentTick >= building.production->completesAt)
|
||||||
@@ -779,8 +863,33 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle: check if a new cycle can start.
|
// Idle: gather the candidate recipes to try. Auto-recipe buildings
|
||||||
|
// (Smelter, Reprocessing Plant) have no selected recipe and try every
|
||||||
|
// recipe of their type in config order, running the first whose inputs
|
||||||
|
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
|
||||||
|
// try only their selected recipe.
|
||||||
|
std::vector<const RecipeDef*> candidates;
|
||||||
|
if (autoRecipe)
|
||||||
|
{
|
||||||
|
for (const RecipeDef& r : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (r.building == building.type && !r.inputs.empty())
|
||||||
|
{
|
||||||
|
candidates.push_back(&r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||||
|
if (recipe)
|
||||||
|
{
|
||||||
|
candidates.push_back(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RecipeDef* recipe : candidates)
|
||||||
|
{
|
||||||
// 1. All required inputs present?
|
// 1. All required inputs present?
|
||||||
bool inputsOk = true;
|
bool inputsOk = true;
|
||||||
for (const RecipeIngredient& ing : recipe->inputs)
|
for (const RecipeIngredient& ing : recipe->inputs)
|
||||||
@@ -835,10 +944,12 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Production prod;
|
Production prod;
|
||||||
prod.recipeId = building.recipeId;
|
prod.recipeId = recipe->id;
|
||||||
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
|
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
|
||||||
prod.chosenOutputs = std::move(chosen);
|
prod.chosenOutputs = std::move(chosen);
|
||||||
building.production = std::move(prod);
|
building.production = std::move(prod);
|
||||||
|
break; // At most one cycle starts per tick.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,10 @@ private:
|
|||||||
const ShipDef* findShipDef(const std::string& id) const;
|
const ShipDef* findShipDef(const std::string& id) const;
|
||||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
||||||
|
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
||||||
|
// caps span the union of every recipe of the building's type; no player
|
||||||
|
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
|
void initAutoBuffers(Building& b) const;
|
||||||
void initShipyardBuffers(Building& b) const;
|
void initShipyardBuffers(Building& b) const;
|
||||||
void initSalvageBayBuffer(Building& b) const;
|
void initSalvageBayBuffer(Building& b) const;
|
||||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
std::vector<Port> computeInputPorts(const Building& b) const;
|
||||||
|
|||||||
@@ -474,7 +474,8 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
|
|||||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
||||||
REQUIRE(bs.productionBuildingCount() == 2);
|
REQUIRE(bs.productionBuildingCount() == 2);
|
||||||
|
|
||||||
// Neither has a recipe selected, so neither has an active cycle.
|
// Neither is producing yet: the miner has no recipe selected, and the
|
||||||
|
// smelter (auto-recipe, REQ-BLD-SMELTER) has no input feeding it.
|
||||||
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||||
|
|
||||||
bs.setRecipe(minerId, "mine_iron_ore");
|
bs.setRecipe(minerId, "mine_iron_ore");
|
||||||
@@ -542,7 +543,8 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
|||||||
// Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1).
|
// Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1).
|
||||||
// Output port (2,1) East. Input port example: (2,0) West.
|
// Output port (2,1) East. Input port example: (2,0) West.
|
||||||
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
||||||
bs.setRecipe(sid, "iron_ingot");
|
// Smelters have no recipe selection (REQ-BLD-SMELTER); they auto-accept any
|
||||||
|
// ore/scrap that is an input to a smelter recipe.
|
||||||
|
|
||||||
// Complete construction (15s → tick 450+1 = 451 ticks).
|
// Complete construction (15s → tick 450+1 = 451 ticks).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
@@ -563,6 +565,105 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
|||||||
REQUIRE(it->second >= 1);
|
REQUIRE(it->second >= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A smelter auto-selects the matching recipe for whatever it is fed, with no
|
||||||
|
// player recipe selection (REQ-BLD-SMELTER).
|
||||||
|
TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
||||||
|
"[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
|
|
||||||
|
// Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a
|
||||||
|
// west-flowing belt at input port (2,0).
|
||||||
|
belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
|
for (int i = 0; i < 2; ++i)
|
||||||
|
{
|
||||||
|
belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
|
belts.tick();
|
||||||
|
bs.tickBeltPull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// iron_ingot recipe cycle is 2s; run to completion.
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
|
||||||
|
|
||||||
|
const Building* b = bs.findBuilding(sid);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
bool hasIronIngot = false;
|
||||||
|
for (const Item& item : b->outputBuffer.items)
|
||||||
|
{
|
||||||
|
if (item.type.id == "iron_ingot") { hasIronIngot = true; }
|
||||||
|
}
|
||||||
|
REQUIRE(hasIronIngot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// With mixed inputs, the smelter runs whichever recipe is currently satisfiable
|
||||||
|
// and leaves an incomplete batch of another input waiting (see the union-of-
|
||||||
|
// inputs caps in initAutoBuffers).
|
||||||
|
TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete batch waits",
|
||||||
|
"[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
|
|
||||||
|
// Feed 1 iron_ore (iron_ingot needs 2 — incomplete) then 2 copper_ore
|
||||||
|
// (copper_ingot needs 2 — satisfiable) via the west-flowing input belt.
|
||||||
|
belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
|
const char* fed[] = { "iron_ore", "copper_ore", "copper_ore" };
|
||||||
|
for (const char* id : fed)
|
||||||
|
{
|
||||||
|
belts.tryPutItem(QPoint(2, 0), makeItem(id));
|
||||||
|
belts.tick();
|
||||||
|
bs.tickBeltPull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// copper_ingot cycle is 2.5s; run to completion.
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
|
||||||
|
|
||||||
|
const Building* b = bs.findBuilding(sid);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
|
||||||
|
// Copper was smelted; the lone iron_ore still waits for a second unit.
|
||||||
|
bool hasCopperIngot = false;
|
||||||
|
for (const Item& item : b->outputBuffer.items)
|
||||||
|
{
|
||||||
|
if (item.type.id == "copper_ingot") { hasCopperIngot = true; }
|
||||||
|
}
|
||||||
|
REQUIRE(hasCopperIngot);
|
||||||
|
|
||||||
|
const std::map<ItemType, int>::const_iterator ironIt =
|
||||||
|
b->inputBuffer.counts.find(ItemType{"iron_ore"});
|
||||||
|
REQUIRE(ironIt != b->inputBuffer.counts.end());
|
||||||
|
REQUIRE(ironIt->second == 1);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Belt push → belt tile
|
// Belt push → belt tile
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -664,7 +765,8 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
|
|||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
||||||
QPoint(0, 0), Rotation::East, 0);
|
QPoint(0, 0), Rotation::East, 0);
|
||||||
bs.setRecipe(id, "reprocessing_cycle");
|
// Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
|
||||||
|
// single reprocessing recipe is applied automatically on completion.
|
||||||
|
|
||||||
// Complete construction (25s).
|
// Complete construction (25s).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
@@ -695,7 +797,8 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
|||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
||||||
QPoint(0, 0), Rotation::East, 0);
|
QPoint(0, 0), Rotation::East, 0);
|
||||||
bs.setRecipe(id, "reprocessing_cycle");
|
// Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
|
||||||
|
// single reprocessing recipe is applied automatically on completion.
|
||||||
|
|
||||||
// Complete construction (25s).
|
// Complete construction (25s).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
|||||||
REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0));
|
REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0));
|
||||||
REQUIRE(cfg.world.scrapPerThreat == Approx(1.0));
|
REQUIRE(cfg.world.scrapPerThreat == Approx(1.0));
|
||||||
|
|
||||||
|
// Optional header building blocks tooltip (REQ-UI-BLOCKS-TOOLTIP).
|
||||||
|
REQUIRE(cfg.world.buildingBlocksTooltip.has_value());
|
||||||
|
REQUIRE(*cfg.world.buildingBlocksTooltip ==
|
||||||
|
"Spend building blocks to build; deliver them to the HQ to gain more.");
|
||||||
|
|
||||||
// Spot-check that a config-derived formula computes as expected.
|
// Spot-check that a config-derived formula computes as expected.
|
||||||
// threat_rate_formula = "x": evaluates to the input value.
|
// threat_rate_formula = "x": evaluates to the input value.
|
||||||
REQUIRE(cfg.world.waves.threatRateFormula.evaluate(1.0) == Approx(1.0));
|
REQUIRE(cfg.world.waves.threatRateFormula.evaluate(1.0) == Approx(1.0));
|
||||||
@@ -113,6 +118,12 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
|||||||
REQUIRE(salvageBayIt->outputBufferCapacity.has_value());
|
REQUIRE(salvageBayIt->outputBufferCapacity.has_value());
|
||||||
REQUIRE(*salvageBayIt->outputBufferCapacity == 20);
|
REQUIRE(*salvageBayIt->outputBufferCapacity == 20);
|
||||||
|
|
||||||
|
// Optional per-building tooltip (REQ-UI-BUILD-TOOLTIP): the Salvage Bay
|
||||||
|
// defines one; the Miner leaves it unset.
|
||||||
|
REQUIRE(salvageBayIt->tooltip.has_value());
|
||||||
|
REQUIRE(*salvageBayIt->tooltip == "Drop-off point for salvage ships.");
|
||||||
|
REQUIRE_FALSE(minerIt->tooltip.has_value());
|
||||||
|
|
||||||
// recipes.toml — reprocessing cycle has three weighted outputs.
|
// recipes.toml — reprocessing cycle has three weighted outputs.
|
||||||
const auto reproIt = std::find_if(
|
const auto reproIt = std::find_if(
|
||||||
cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
|
cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
|
||||||
@@ -144,6 +155,21 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
|||||||
REQUIRE(salvageShipIt != cfg.ships.ships.end());
|
REQUIRE(salvageShipIt != cfg.ships.ships.end());
|
||||||
REQUIRE(salvageShipIt->defaultModules.empty());
|
REQUIRE(salvageShipIt->defaultModules.empty());
|
||||||
|
|
||||||
|
// modules.toml — optional per-module tooltip (REQ-MOD-UI-MODULE-TOOLTIP):
|
||||||
|
// armor_plate defines one; salvager leaves it unset.
|
||||||
|
const auto armorPlateIt = std::find_if(
|
||||||
|
cfg.modules.modules.begin(), cfg.modules.modules.end(),
|
||||||
|
[](const ModuleDef& m) { return m.id == "armor_plate"; });
|
||||||
|
REQUIRE(armorPlateIt != cfg.modules.modules.end());
|
||||||
|
REQUIRE(armorPlateIt->tooltip.has_value());
|
||||||
|
REQUIRE(*armorPlateIt->tooltip == "Adds a large flat bonus to hit points.");
|
||||||
|
|
||||||
|
const auto salvagerModuleIt = std::find_if(
|
||||||
|
cfg.modules.modules.begin(), cfg.modules.modules.end(),
|
||||||
|
[](const ModuleDef& m) { return m.id == "salvager"; });
|
||||||
|
REQUIRE(salvagerModuleIt != cfg.modules.modules.end());
|
||||||
|
REQUIRE_FALSE(salvagerModuleIt->tooltip.has_value());
|
||||||
|
|
||||||
// stations.toml
|
// stations.toml
|
||||||
REQUIRE(cfg.stations.playerStation.level == 5);
|
REQUIRE(cfg.stations.playerStation.level == 5);
|
||||||
REQUIRE(cfg.stations.playerStation.hpFormula.evaluate(5.0) == Approx(500.0)); // 300 + 40*5
|
REQUIRE(cfg.stations.playerStation.hpFormula.evaluate(5.0) == Approx(500.0)); // 300 + 40*5
|
||||||
|
|||||||
@@ -157,14 +157,8 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
|||||||
{
|
{
|
||||||
const Building* b = m_sim->buildings().findBuilding(id);
|
const Building* b = m_sim->buildings().findBuilding(id);
|
||||||
if (!b) { continue; }
|
if (!b) { continue; }
|
||||||
const bool placeable = [&]() {
|
const BuildingDef* def = m_config->buildings.findBuildingDef(b->type);
|
||||||
for (const BuildingDef& def : m_config->buildings.buildings)
|
if (def && def->playerPlaceable) { entries.push_back({ b }); }
|
||||||
{
|
|
||||||
if (def.type == b->type) { return def.playerPlaceable; }
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}();
|
|
||||||
if (placeable) { entries.push_back({ b }); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries.empty()) { return Blueprint{}; }
|
if (entries.empty()) { return Blueprint{}; }
|
||||||
@@ -213,14 +207,8 @@ int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const
|
|||||||
int total = 0;
|
int total = 0;
|
||||||
for (const BlueprintBuilding& bb : bp.buildings)
|
for (const BlueprintBuilding& bb : bp.buildings)
|
||||||
{
|
{
|
||||||
for (const BuildingDef& def : m_config->buildings.buildings)
|
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
|
||||||
{
|
if (def) { total += def->cost; }
|
||||||
if (def.type == bb.type)
|
|
||||||
{
|
|
||||||
total += def.cost;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
@@ -239,7 +227,7 @@ void BlueprintPanel::rebuildButtons()
|
|||||||
{
|
{
|
||||||
const Blueprint& bp = m_blueprints[static_cast<std::size_t>(i)];
|
const Blueprint& bp = m_blueprints[static_cast<std::size_t>(i)];
|
||||||
const int cost = computeBlueprintCost(bp);
|
const int cost = computeBlueprintCost(bp);
|
||||||
const QString label = bp.name + "\n" + tr("%1 Blocks").arg(cost);
|
const QString label = bp.name + "\n" + tr("%1 Building Blocks").arg(cost);
|
||||||
|
|
||||||
QWidget* row = new QWidget(m_buttonsContainer);
|
QWidget* row = new QWidget(m_buttonsContainer);
|
||||||
QHBoxLayout* rowLayout = new QHBoxLayout(row);
|
QHBoxLayout* rowLayout = new QHBoxLayout(row);
|
||||||
|
|||||||
@@ -38,10 +38,14 @@ BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
|
|||||||
m_costs[def.type] = def.cost;
|
m_costs[def.type] = def.cost;
|
||||||
|
|
||||||
const QString label = QString::fromStdString(toDisplayName(def.id))
|
const QString label = QString::fromStdString(toDisplayName(def.id))
|
||||||
+ "\n" + tr("%1 Blocks").arg(def.cost);
|
+ "\n" + tr("%1 Building Blocks").arg(def.cost);
|
||||||
QPushButton* btn = new QPushButton(label, this);
|
QPushButton* btn = new QPushButton(label, this);
|
||||||
btn->setCheckable(true);
|
btn->setCheckable(true);
|
||||||
btn->setFixedHeight(48);
|
btn->setFixedHeight(48);
|
||||||
|
if (def.tooltip)
|
||||||
|
{
|
||||||
|
btn->setToolTip(QString::fromStdString(*def.tooltip));
|
||||||
|
}
|
||||||
layout->addWidget(btn, row, col);
|
layout->addWidget(btn, row, col);
|
||||||
|
|
||||||
const int idx = static_cast<int>(m_buttons.size());
|
const int idx = static_cast<int>(m_buttons.size());
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||||
const int HeaderBar::kSpeedCount = 5;
|
const int HeaderBar::kSpeedCount = 5;
|
||||||
|
|
||||||
HeaderBar::HeaderBar(QWidget* parent)
|
HeaderBar::HeaderBar(const GameConfig* config, QWidget* parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
@@ -25,7 +25,12 @@ HeaderBar::HeaderBar(QWidget* parent)
|
|||||||
layout->setSpacing(8);
|
layout->setSpacing(8);
|
||||||
|
|
||||||
m_timeLabel = new QLabel("00:00", this);
|
m_timeLabel = new QLabel("00:00", this);
|
||||||
m_blocksLabel = new QLabel(tr("Blocks: 0"), this);
|
m_blocksLabel = new QLabel(tr("Building Blocks: 0"), this);
|
||||||
|
if (config->world.buildingBlocksTooltip)
|
||||||
|
{
|
||||||
|
m_blocksLabel->setToolTip(
|
||||||
|
QString::fromStdString(*config->world.buildingBlocksTooltip));
|
||||||
|
}
|
||||||
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
|
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
|
||||||
m_bossLabel = new QLabel(tr("Boss Wave #1 Next boss: 5:00"), this);
|
m_bossLabel = new QLabel(tr("Boss Wave #1 Next boss: 5:00"), this);
|
||||||
layout->addWidget(m_timeLabel);
|
layout->addWidget(m_timeLabel);
|
||||||
@@ -81,7 +86,7 @@ void HeaderBar::handleEvent(std::shared_ptr<const TickAdvancedEvent> event)
|
|||||||
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
||||||
{
|
{
|
||||||
m_blocks = event->blocks;
|
m_blocks = event->blocks;
|
||||||
m_blocksLabel->setText(tr("Blocks: %1").arg(event->blocks));
|
m_blocksLabel->setText(tr("Building Blocks: %1").arg(event->blocks));
|
||||||
updateExpandButton();
|
updateExpandButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +98,7 @@ void HeaderBar::handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> eve
|
|||||||
|
|
||||||
void HeaderBar::updateExpandButton()
|
void HeaderBar::updateExpandButton()
|
||||||
{
|
{
|
||||||
m_expandButton->setText(tr("Expand: %1 Blocks").arg(m_expansionCost));
|
m_expandButton->setText(tr("Expand: %1 Building Blocks").arg(m_expansionCost));
|
||||||
m_expandButton->setEnabled(m_blocks >= m_expansionCost);
|
m_expandButton->setEnabled(m_blocks >= m_expansionCost);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "BuildingBlocksChangedEvent.h"
|
#include "BuildingBlocksChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "ExpansionCostChangedEvent.h"
|
#include "ExpansionCostChangedEvent.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
#include "GameSpeedChangedEvent.h"
|
#include "GameSpeedChangedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
@@ -27,7 +28,7 @@ class HeaderBar : public QWidget,
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit HeaderBar(QWidget* parent = nullptr);
|
explicit HeaderBar(const GameConfig* config, QWidget* parent = nullptr);
|
||||||
~HeaderBar() override;
|
~HeaderBar() override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|||||||
@@ -42,15 +42,15 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
setWindowTitle(tr("Dota Factory"));
|
setWindowTitle(tr("Dota Factory"));
|
||||||
resize(1280, 768);
|
resize(1280, 768);
|
||||||
|
|
||||||
m_headerBar = new HeaderBar(this);
|
m_headerBar = new HeaderBar(&sim->config(), this);
|
||||||
|
|
||||||
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, m_configDir,
|
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, m_configDir,
|
||||||
m_replay.get(), this);
|
m_replay.get(), this);
|
||||||
|
|
||||||
m_sidePanel = new QWidget(this);
|
m_sidePanel = new QWidget(this);
|
||||||
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||||
sideLayout->setContentsMargins(0, 0, 0, 0);
|
sideLayout->setContentsMargins(1, 1, 1, 1);
|
||||||
sideLayout->setSpacing(0);
|
sideLayout->setSpacing(1);
|
||||||
|
|
||||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_sidePanel);
|
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_sidePanel);
|
||||||
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_sidePanel);
|
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_sidePanel);
|
||||||
@@ -60,6 +60,20 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
sideLayout->addWidget(m_buildButtonGrid, 1);
|
sideLayout->addWidget(m_buildButtonGrid, 1);
|
||||||
sideLayout->addWidget(m_blueprintPanel, 1);
|
sideLayout->addWidget(m_blueprintPanel, 1);
|
||||||
|
|
||||||
|
// Draw a thin border around each of the three side-panel sections. The class
|
||||||
|
// scoped selectors keep the border on the panels themselves rather than
|
||||||
|
// cascading onto their child widgets; WA_StyledBackground lets the plain
|
||||||
|
// QWidget subclasses honor the stylesheet box (border/background).
|
||||||
|
for (QWidget* panel : { static_cast<QWidget*>(m_selectedBuildingPanel),
|
||||||
|
static_cast<QWidget*>(m_buildButtonGrid),
|
||||||
|
static_cast<QWidget*>(m_blueprintPanel) })
|
||||||
|
{
|
||||||
|
panel->setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
}
|
||||||
|
m_sidePanel->setStyleSheet(QStringLiteral(
|
||||||
|
"SelectedBuildingPanel, BuildButtonGrid, BlueprintPanel {"
|
||||||
|
" border: 1px solid palette(mid); }"));
|
||||||
|
|
||||||
m_gameWorldView->setFocus();
|
m_gameWorldView->setFocus();
|
||||||
|
|
||||||
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
|
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ QString itemLine(const std::string& itemId, int amount)
|
|||||||
{
|
{
|
||||||
return QStringLiteral(" ")
|
return QStringLiteral(" ")
|
||||||
+ QString::fromStdString(toDisplayName(itemId))
|
+ QString::fromStdString(toDisplayName(itemId))
|
||||||
+ QStringLiteral(" ×") + QString::number(amount);
|
+ QStringLiteral(" x ") + QString::number(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString shipTooltip(const ShipDef& def)
|
QString shipTooltip(const ShipDef& def)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ QString itemLine(const std::string& itemId, int amount)
|
|||||||
{
|
{
|
||||||
return QStringLiteral(" ")
|
return QStringLiteral(" ")
|
||||||
+ QString::fromStdString(toDisplayName(itemId))
|
+ QString::fromStdString(toDisplayName(itemId))
|
||||||
+ QStringLiteral(" ×") + QString::number(amount);
|
+ QStringLiteral(" x ") + QString::number(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -78,6 +78,25 @@ bool isProductionBuilding(BuildingType type)
|
|||||||
|| type == BuildingType::Shipyard;
|
|| type == BuildingType::Shipyard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buildings that expose a player recipe/schematic selection control
|
||||||
|
// (REQ-UI-SELECT-BUTTON): Miner ore type, Assembler recipe, Shipyard schematic.
|
||||||
|
// The Smelter and Reprocessing Plant auto-process and offer no selection
|
||||||
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
|
bool hasRecipeSelection(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Miner
|
||||||
|
|| type == BuildingType::Assembler
|
||||||
|
|| type == BuildingType::Shipyard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-recipe buildings have no selected recipe; their production is driven by
|
||||||
|
// whatever inputs they receive.
|
||||||
|
bool isAutoRecipeBuilding(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Smelter
|
||||||
|
|| type == BuildingType::ReprocessingPlant;
|
||||||
|
}
|
||||||
|
|
||||||
bool isBeltLike(BuildingType type)
|
bool isBeltLike(BuildingType type)
|
||||||
{
|
{
|
||||||
return type == BuildingType::Belt || type == BuildingType::Splitter
|
return type == BuildingType::Belt || type == BuildingType::Splitter
|
||||||
@@ -265,7 +284,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
|||||||
m_titleLabel->show();
|
m_titleLabel->show();
|
||||||
m_buffersLabel->show();
|
m_buffersLabel->show();
|
||||||
|
|
||||||
if (isProductionBuilding(type))
|
if (hasRecipeSelection(type))
|
||||||
{
|
{
|
||||||
const std::vector<RecipeSelectionOption> options =
|
const std::vector<RecipeSelectionOption> options =
|
||||||
buildRecipeSelectionOptions(type, *m_sim, *m_config);
|
buildRecipeSelectionOptions(type, *m_sim, *m_config);
|
||||||
@@ -383,6 +402,21 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
? findShipDef(b->recipeId)
|
? findShipDef(b->recipeId)
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
|
||||||
|
// Auto-recipe buildings (Smelter, Reprocessing Plant) have no selected
|
||||||
|
// recipe; while a cycle runs, resolve the recipe actually in production so
|
||||||
|
// the cycle time and progress can be shown (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
|
if (!recipe && isAutoRecipeBuilding(b->type) && b->production.has_value())
|
||||||
|
{
|
||||||
|
for (const RecipeDef& r : m_config->recipes.recipes)
|
||||||
|
{
|
||||||
|
if (r.id == b->production->recipeId && r.building == b->type)
|
||||||
|
{
|
||||||
|
recipe = &r;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString bufText;
|
QString bufText;
|
||||||
|
|
||||||
if (!b->inputBuffer.counts.empty())
|
if (!b->inputBuffer.counts.empty())
|
||||||
@@ -469,7 +503,10 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isProductionBuilding(b->type) && (recipe || shipDef))
|
if (isProductionBuilding(b->type)
|
||||||
|
&& (recipe || shipDef || isAutoRecipeBuilding(b->type)))
|
||||||
|
{
|
||||||
|
if (recipe || shipDef)
|
||||||
{
|
{
|
||||||
double durationSeconds = recipe
|
double durationSeconds = recipe
|
||||||
? recipe->durationSeconds
|
? recipe->durationSeconds
|
||||||
@@ -507,6 +544,13 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
bufText += tr("Progress: idle\n");
|
bufText += tr("Progress: idle\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Auto-recipe building with no active cycle: no single recipe to
|
||||||
|
// show a cycle time for.
|
||||||
|
bufText += tr("Progress: idle\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_buffersLabel->setText(bufText);
|
m_buffersLabel->setText(bufText);
|
||||||
|
|
||||||
@@ -522,11 +566,21 @@ void SelectedBuildingPanel::updateShipyardLayoutWidgets(
|
|||||||
const std::string& recipeId,
|
const std::string& recipeId,
|
||||||
const std::optional<ShipLayoutConfig>& shipLayout)
|
const std::optional<ShipLayoutConfig>& shipLayout)
|
||||||
{
|
{
|
||||||
const ShipDef* shipDef = (type == BuildingType::Shipyard)
|
// The preview and Configure button are shipyard-only controls; hide them
|
||||||
? findShipDef(recipeId)
|
// entirely for other building types.
|
||||||
: nullptr;
|
if (type != BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
m_layoutPreview->hide();
|
||||||
|
m_configureLayoutBtn->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (shipDef && !shipDef->layout.empty())
|
const ShipDef* shipDef = findShipDef(recipeId);
|
||||||
|
const bool hasSchematic = shipDef && !shipDef->layout.empty();
|
||||||
|
|
||||||
|
// Always show the preview and Configure button for a shipyard; they are only
|
||||||
|
// enabled once a schematic is selected (REQ-MOD-UI-PREVIEW).
|
||||||
|
if (hasSchematic)
|
||||||
{
|
{
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
if (shipLayout.has_value())
|
if (shipLayout.has_value())
|
||||||
@@ -535,14 +589,16 @@ void SelectedBuildingPanel::updateShipyardLayoutWidgets(
|
|||||||
}
|
}
|
||||||
m_layoutPreview->setShipAndLayout(
|
m_layoutPreview->setShipAndLayout(
|
||||||
shipDef->layout, layout, &m_config->modules.modules);
|
shipDef->layout, layout, &m_config->modules.modules);
|
||||||
m_layoutPreview->show();
|
|
||||||
m_configureLayoutBtn->show();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_layoutPreview->hide();
|
m_layoutPreview->showPlaceholder();
|
||||||
m_configureLayoutBtn->hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_layoutPreview->setEnabled(hasSchematic);
|
||||||
|
m_configureLayoutBtn->setEnabled(hasSchematic);
|
||||||
|
m_layoutPreview->show();
|
||||||
|
m_configureLayoutBtn->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
|
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
|
||||||
@@ -650,6 +706,7 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool hasBelt = false;
|
bool hasBelt = false;
|
||||||
|
int totalCost = 0;
|
||||||
QString text;
|
QString text;
|
||||||
for (const std::pair<const BuildingType, int>& entry : counts)
|
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||||
{
|
{
|
||||||
@@ -659,7 +716,15 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
|
|||||||
{
|
{
|
||||||
hasBelt = true;
|
hasBelt = true;
|
||||||
}
|
}
|
||||||
|
// Total placement cost counts only player-placeable buildings; the HQ
|
||||||
|
// and defence stations are excluded (REQ-UI-MULTI-SELECTION).
|
||||||
|
const BuildingDef* def = m_config->buildings.findBuildingDef(entry.first);
|
||||||
|
if (def && def->playerPlaceable)
|
||||||
|
{
|
||||||
|
totalCost += def->cost * entry.second;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
text += tr("Total: %1 Building Blocks").arg(totalCost);
|
||||||
m_titleLabel->setText(text.trimmed());
|
m_titleLabel->setText(text.trimmed());
|
||||||
m_titleLabel->show();
|
m_titleLabel->show();
|
||||||
|
|
||||||
|
|||||||
@@ -461,6 +461,10 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
|||||||
QPushButton* btn = new QPushButton(label, this);
|
QPushButton* btn = new QPushButton(label, this);
|
||||||
btn->setCheckable(true);
|
btn->setCheckable(true);
|
||||||
btn->setFixedHeight(48);
|
btn->setFixedHeight(48);
|
||||||
|
if (def.tooltip)
|
||||||
|
{
|
||||||
|
btn->setToolTip(QString::fromStdString(*def.tooltip));
|
||||||
|
}
|
||||||
buttonGrid->addWidget(btn, row, col);
|
buttonGrid->addWidget(btn, row, col);
|
||||||
m_moduleButtons.push_back(btn);
|
m_moduleButtons.push_back(btn);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ namespace
|
|||||||
|
|
||||||
const int kCellSize = 8;
|
const int kCellSize = 8;
|
||||||
|
|
||||||
|
// Size of the empty placeholder box shown when no schematic is selected.
|
||||||
|
const int kPlaceholderWidth = 64;
|
||||||
|
const int kPlaceholderHeight = 40;
|
||||||
|
|
||||||
const ModuleDef* findModuleDef(const std::vector<ModuleDef>& modules,
|
const ModuleDef* findModuleDef(const std::vector<ModuleDef>& modules,
|
||||||
const std::string& id)
|
const std::string& id)
|
||||||
{
|
{
|
||||||
@@ -90,14 +94,28 @@ void ShipLayoutPreview::clear()
|
|||||||
m_modules = nullptr;
|
m_modules = nullptr;
|
||||||
m_rows = 0;
|
m_rows = 0;
|
||||||
m_cols = 0;
|
m_cols = 0;
|
||||||
|
m_placeholder = false;
|
||||||
setFixedSize(0, 0);
|
setFixedSize(0, 0);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ShipLayoutPreview::showPlaceholder()
|
||||||
|
{
|
||||||
|
m_grid.clear();
|
||||||
|
m_placedModules.clear();
|
||||||
|
m_modules = nullptr;
|
||||||
|
m_rows = 0;
|
||||||
|
m_cols = 0;
|
||||||
|
m_placeholder = true;
|
||||||
|
setFixedSize(kPlaceholderWidth, kPlaceholderHeight);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
void ShipLayoutPreview::setShipAndLayout(const std::vector<std::string>& shipLayout,
|
void ShipLayoutPreview::setShipAndLayout(const std::vector<std::string>& shipLayout,
|
||||||
const ShipLayoutConfig& layout,
|
const ShipLayoutConfig& layout,
|
||||||
const std::vector<ModuleDef>* modules)
|
const std::vector<ModuleDef>* modules)
|
||||||
{
|
{
|
||||||
|
m_placeholder = false;
|
||||||
m_modules = modules;
|
m_modules = modules;
|
||||||
m_placedModules = layout.placedModules;
|
m_placedModules = layout.placedModules;
|
||||||
m_rows = static_cast<int>(shipLayout.size());
|
m_rows = static_cast<int>(shipLayout.size());
|
||||||
@@ -156,6 +174,18 @@ void ShipLayoutPreview::setShipAndLayout(const std::vector<std::string>& shipLay
|
|||||||
|
|
||||||
void ShipLayoutPreview::paintEvent(QPaintEvent* /*event*/)
|
void ShipLayoutPreview::paintEvent(QPaintEvent* /*event*/)
|
||||||
{
|
{
|
||||||
|
if (m_placeholder)
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
const QRect box = rect().adjusted(0, 0, -1, -1);
|
||||||
|
painter.fillRect(box, QColor(40, 40, 40));
|
||||||
|
painter.setPen(QColor(128, 128, 128));
|
||||||
|
painter.drawRect(box);
|
||||||
|
painter.drawText(box, Qt::AlignCenter, tr("No schematic"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_rows == 0 || m_cols == 0)
|
if (m_rows == 0 || m_cols == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ public:
|
|||||||
const std::vector<ModuleDef>* modules);
|
const std::vector<ModuleDef>* modules);
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
|
// Shows an empty placeholder box (no ship layout) so the preview stays
|
||||||
|
// visible while no schematic is selected (REQ-MOD-UI-PREVIEW).
|
||||||
|
void showPlaceholder();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
@@ -35,4 +39,5 @@ private:
|
|||||||
const std::vector<ModuleDef>* m_modules;
|
const std::vector<ModuleDef>* m_modules;
|
||||||
int m_rows;
|
int m_rows;
|
||||||
int m_cols;
|
int m_cols;
|
||||||
|
bool m_placeholder = false;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user