From 10c5ad678f363394a4612c40a60110c6b5274279 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Sat, 13 Jun 2026 21:50:00 +0200 Subject: [PATCH] derive threat cost dynamically --- bin/app/data/config/modules.toml | 11 - bin/app/data/config/ships.toml | 3 - bin/test/data/config/modules.toml | 10 - bin/test/data/config/ships.toml | 12 - docs/requirements.md | 27 +- src/lib/config/ConfigLoader.cpp | 10 +- src/lib/config/GameConfig.h | 2 + src/lib/config/ModulesConfig.h | 1 - src/lib/config/ShipsConfig.h | 8 - src/lib/eventsystem/event/CMakeLists.txt | 1 + .../eventsystem/event/DebugDrawToggledEvent.h | 14 + src/lib/sim/CMakeLists.txt | 2 + src/lib/sim/ThreatCostCalculator.cpp | 247 ++++++++++++++++++ src/lib/sim/ThreatCostCalculator.h | 22 ++ src/lib/sim/WaveSystem.cpp | 4 +- src/test/CMakeLists.txt | 1 + src/test/ModuleConfigTest.cpp | 1 - src/test/ThreatCostCalculatorTest.cpp | 110 ++++++++ src/test/WaveSystemTest.cpp | 22 +- src/ui/GameWorldView.cpp | 7 + src/ui/GameWorldView.h | 2 + src/ui/MainWindow.cpp | 1 + src/ui/SelectedBuildingPanel.cpp | 20 ++ src/ui/SelectedBuildingPanel.h | 6 +- src/ui/ShipLayoutDialog.cpp | 3 + src/ui/ShipLayoutDialog.h | 2 + src/ui/ShipStatsPanel.cpp | 22 ++ src/ui/ShipStatsPanel.h | 6 + 28 files changed, 498 insertions(+), 79 deletions(-) create mode 100644 src/lib/eventsystem/event/DebugDrawToggledEvent.h create mode 100644 src/lib/sim/ThreatCostCalculator.cpp create mode 100644 src/lib/sim/ThreatCostCalculator.h create mode 100644 src/test/ThreatCostCalculatorTest.cpp diff --git a/bin/app/data/config/modules.toml b/bin/app/data/config/modules.toml index e6982af..af94ec4 100644 --- a/bin/app/data/config/modules.toml +++ b/bin/app/data/config/modules.toml @@ -5,7 +5,6 @@ surface_mask = ["OO"] materials = [{item = "armor_plate_module", amount = 1}] player_production_level = 1 production_time_seconds = 3 -threat_cost = 20.0 fill_color = "#808080" glyph = "A" @@ -20,7 +19,6 @@ surface_mask = ["O"] materials = [{item = "sensor_booster_module", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 1.0 fill_color = "#40A0FF" glyph = "S" @@ -35,7 +33,6 @@ surface_mask = ["O"] materials = [{item = "manuvering_thrusters_module", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 1.0 fill_color = "#40A0FF" glyph = "Mt" @@ -51,7 +48,6 @@ surface_mask = ["O"] materials = [{item = "afterburner_module", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 1.0 fill_color = "#40A0FF" glyph = "Ab" @@ -70,7 +66,6 @@ surface_mask = [ materials = [{item = "weapon_upgrade_module", amount = 1}] player_production_level = 1 production_time_seconds = 4 -threat_cost = 10.0 fill_color = "#FF4040" glyph = "Wu" @@ -88,7 +83,6 @@ surface_mask = [ materials = [{item = "weapon_primer_module", amount = 1}] player_production_level = 1 production_time_seconds = 4 -threat_cost = 10.0 fill_color = "#FF4040" glyph = "Wp" @@ -106,7 +100,6 @@ surface_mask = [ materials = [{item = "weapon_stabilizer_module", amount = 1}] player_production_level = 1 production_time_seconds = 4 -threat_cost = 10.0 fill_color = "#FF4040" glyph = "Ws" @@ -122,7 +115,6 @@ surface_mask = ["O"] materials = [{item = "iron_ore", amount = 1}] player_production_level = 1 production_time_seconds = 0.5 -threat_cost = 5.0 fill_color = "#FF8040" glyph = "L" @@ -141,7 +133,6 @@ surface_mask = [ materials = [{item = "laser_cannon_s_module", amount = 1}] player_production_level = 1 production_time_seconds = 0.5 -threat_cost = 30.0 fill_color = "#FF8040" glyph = "L" @@ -158,7 +149,6 @@ surface_mask = ["O"] materials = [{item = "salvager_module", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 0.0 fill_color = "#AACC44" glyph = "Sv" @@ -175,7 +165,6 @@ surface_mask = ["O"] materials = [{item = "repair_tool_module", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 0.0 fill_color = "#66CCFF" glyph = "Rp" diff --git a/bin/app/data/config/ships.toml b/bin/app/data/config/ships.toml index 3969221..3e07a2f 100644 --- a/bin/app/data/config/ships.toml +++ b/bin/app/data/config/ships.toml @@ -9,9 +9,6 @@ materials = [{item = "iron_ore", amount = 1}] player_production_level = 1 production_time_seconds = 5 -[ship.threat] -cost_formula = "10" - [ship.health] hp_formula = "3" diff --git a/bin/test/data/config/modules.toml b/bin/test/data/config/modules.toml index cdd00fb..3c8e193 100644 --- a/bin/test/data/config/modules.toml +++ b/bin/test/data/config/modules.toml @@ -5,7 +5,6 @@ surface_mask = ["OO"] materials = [{item = "iron_ingot", amount = 2}] player_production_level = 1 production_time_seconds = 3 -threat_cost = 2.0 fill_color = "#808080" glyph = "A" @@ -19,7 +18,6 @@ surface_mask = ["O"] materials = [{item = "circuit_board", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 1.0 fill_color = "#40A0FF" glyph = "S" @@ -33,7 +31,6 @@ surface_mask = ["O"] materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}] player_production_level = 1 production_time_seconds = 4 -threat_cost = 3.0 fill_color = "#FF4040" glyph = "W" @@ -47,7 +44,6 @@ surface_mask = ["O"] materials = [{item = "iron_ingot", amount = 1}] player_production_level = 1 production_time_seconds = 5 -threat_cost = 5.0 fill_color = "#FF8040" glyph = "L" @@ -63,7 +59,6 @@ surface_mask = ["OO"] materials = [{item = "iron_ingot", amount = 2}] player_production_level = 1 production_time_seconds = 5 -threat_cost = 0.0 fill_color = "#AACC44" glyph = "Sv" @@ -79,7 +74,6 @@ surface_mask = ["O"] materials = [{item = "circuit_board", amount = 2}] player_production_level = 1 production_time_seconds = 5 -threat_cost = 0.0 fill_color = "#66CCFF" glyph = "Rp" @@ -94,7 +88,6 @@ surface_mask = ["O"] materials = [{item = "iron_ingot", amount = 1}] player_production_level = 1 production_time_seconds = 4 -threat_cost = 1.0 fill_color = "#FF4040" glyph = "Wp" @@ -108,7 +101,6 @@ surface_mask = ["O"] materials = [{item = "iron_ingot", amount = 1}] player_production_level = 1 production_time_seconds = 4 -threat_cost = 1.0 fill_color = "#FF4040" glyph = "Ws" @@ -123,7 +115,6 @@ surface_mask = ["O"] materials = [{item = "iron_ingot", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 1.0 fill_color = "#40A0FF" glyph = "Ab" @@ -138,7 +129,6 @@ surface_mask = ["O"] materials = [{item = "iron_ingot", amount = 1}] player_production_level = 1 production_time_seconds = 2 -threat_cost = 1.0 fill_color = "#40A0FF" glyph = "Mt" diff --git a/bin/test/data/config/ships.toml b/bin/test/data/config/ships.toml index 7009ade..f0b6551 100644 --- a/bin/test/data/config/ships.toml +++ b/bin/test/data/config/ships.toml @@ -9,9 +9,6 @@ materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount player_production_level = 3 production_time_seconds = 10 -[ship.threat] -cost_formula = "5 + 1*x" - [ship.health] hp_formula = "40 + 5*x" @@ -40,9 +37,6 @@ materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount player_production_level = 5 production_time_seconds = 20 -[ship.threat] -cost_formula = "10 + 2*x" - [ship.health] hp_formula = "120 + 15*x" @@ -70,9 +64,6 @@ materials = [{item = "iron_ingot", amount = 4}] player_production_level = 3 production_time_seconds = 10 -[ship.threat] -cost_formula = "0" - [ship.health] hp_formula = "40 + 4*x" @@ -100,9 +91,6 @@ materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount player_production_level = 3 production_time_seconds = 15 -[ship.threat] -cost_formula = "0" - [ship.health] hp_formula = "60 + 5*x" diff --git a/docs/requirements.md b/docs/requirements.md index 6aa5e2a..83abf14 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -7,8 +7,8 @@ Config files use the TOML format. The following config files drive game paramete - **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval. - **buildings.toml** — building block cost and construction time per building type. - **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). -- **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 formulas of ship level, required build materials, threat cost formula, player production level, 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), 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, initial player production level, production time, threat cost, 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), 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). +- **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 formulas of ship level, required build materials, player production level, 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), a layout grid defining the ship's module slots, a `scrap_drop` loot value, 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, initial player production level, 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), 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. - **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; beam color and 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. @@ -182,7 +182,6 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - `player_production_level` — initial level for this module type; used as `x` in its stat formulas. Incremented by 1 on each duplicate schematic drop (REQ-DEF-SCHEMATIC-DROP). - `unlock_at_station_level` — the enemy defence station level at which this module's schematic becomes available for unlock; -1 means the player starts with the module schematic already unlocked. - `production_time_seconds` — time added to the ship's production cycle per instance. - - `threat_cost` — threat cost added to the ship's threat cost per instance. - `fill_color` — fill color used to render this module's cells in the layout grid. - `glyph` — single character rendered on this module's cells in the layout grid and preview widget. - An optional **capability section** (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas. A module with base stat formulas is a capability module — each placed instance grants the ship an independent weapon, salvage bay, or repair tool with its own state (cooldown, target, cargo). A ship may have multiple capability module instances of the same or different types. Base stat formulas per capability type: @@ -203,7 +202,19 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - REQ-MOD-MATERIALS: The total materials required to build a ship are the union of the ship's base `[ship.schematic].materials` and the `materials` of every module instance in the configured layout. Quantities of the same item type are summed. - REQ-MOD-PRODUCTION-TIME: The total production time is the ship's base `[ship.schematic].production_time_seconds` plus the sum of `production_time_seconds` for every module instance in the configured layout. -- REQ-MOD-THREAT: The total threat cost of a ship is the ship's base `[ship.threat].cost_formula` evaluated at the ship's level, plus the sum of `threat_cost` for every module instance in the configured layout. +- REQ-MOD-THREAT: The threat cost of a ship is dynamically derived from the accumulated total production time required to produce that ship from scratch. One second of production time equals one threat. The total production time is the sum of: + 1. The ship's base `production_time_seconds`. + 2. The `production_time_seconds` of every module instance in the configured layout. + 3. For every material required (the union of the ship's base materials and all module instance materials, with quantities summed per item type): the recursive production time of that material multiplied by the required quantity (see REQ-THREAT-ITEM). + +- REQ-THREAT-ITEM: The threat value of an item type (in seconds) is determined by the recipe that produces it: + - **Miner recipe**: the recipe's `duration_seconds`. + - **Smelter recipe**: the recipe's `duration_seconds` plus the sum of each input's threat value multiplied by that input's required quantity. + - **Assembler recipe**: the recipe's `duration_seconds` plus the sum of each input's threat value multiplied by that input's required quantity. + - **Reprocessing-only item** (an item type that has no miner, smelter, or assembler recipe producing it, and is only obtainable via reprocessing): `(scrap_threat × scrap_per_cycle + duration_seconds) / probability`, where `scrap_threat` is the threat value of scrap (see REQ-THREAT-SCRAP), `scrap_per_cycle` is the number of scrap consumed per reprocessing cycle, `duration_seconds` is the reprocessing cycle time, and `probability` is the normalized weight of that item in the reprocessing output pool. + - **Multiple recipes**: if an item type can be produced by more than one non-reprocessing recipe (miner, smelter, or assembler), its threat value is the **maximum** across all such recipes. The reprocessing path is only used when no other recipe exists. + +- REQ-THREAT-SCRAP: The threat value of scrap is derived from the ship schematic with the smallest configured `scrap_drop` value (from `ships.toml [ship.loot].scrap_drop`). Scrap threat = that ship's threat cost (REQ-MOD-THREAT) / that ship's `scrap_drop` value. If multiple schematics share the same smallest `scrap_drop`, any one of them may be used. - REQ-MOD-STAT-CALC: For each stat (on the ship hull or on a capability module instance), the final value is computed as: `final = base × total_multiplier + total_additive`, where: - `base` is the stat's base formula evaluated at the ship's production level (for hull stats) or at the capability module's `player_production_level` (for capability module stats). - `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all passive module instances. Each m_i is evaluated from the module's multiplicative formula at the module's `player_production_level`. @@ -247,6 +258,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC. Each capability module instance uses its own `player_production_level` for formula evaluation. + While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT) for the current layout configuration. This value updates in real time as modules are placed or removed. + - REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog). ### Layout Blueprints @@ -314,8 +327,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - REQ-WAV-BOSS-COUNTER: A global **boss wave counter** `x` starts at 1 at game start and increments by 1 immediately after each boss wave fires. It represents the current boss wave cycle number and is used as the variable in the threat rate and ship level formulas. - REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over real game time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER), clamped to a minimum of 0 (negative formula values are treated as 0). The rate is constant within each boss wave cycle and steps up each time `x` increments. Threat accumulation is paused during quiet windows (REQ-WAV-QUIET). Example: `1*x - 30` yields 0 threat/s when x ≤ 30 and increases linearly beyond that. - REQ-WAV-GAP: At game start and immediately after each normal wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`]. The gap timer does not advance while inside a quiet window (REQ-WAV-QUIET); if a gap would expire inside a quiet window, its expiry is deferred until the quiet window ends. -- REQ-WAV-TRIGGER: When the gap timer expires outside a quiet window, a normal wave is triggered. Ships are selected one at a time: from all schematics whose `threat.cost_formula` evaluates to > 0 at the current enemy ship level, uniformly randomly pick one whose cost fits the remaining threat budget. Repeat until no eligible schematic fits. Any remaining threat carries over to the next normal wave. A longer gap results in a larger wave. Because enemy ship level increases with the boss wave counter (REQ-WAV-SHIP-LEVEL), threat cost per ship rises as the game progresses. -- REQ-WAV-SHIP-LEVEL: Each wave's (normal and boss) enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER). Per-ship stats and threat cost are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS). +- REQ-WAV-TRIGGER: When the gap timer expires outside a quiet window, a normal wave is triggered. Ships are selected one at a time: from all schematics whose threat cost (REQ-MOD-THREAT) is > 0, uniformly randomly pick one whose cost fits the remaining threat budget. For wave ship selection, the threat cost is computed using the schematic's `default_modules` layout (REQ-WAV-DEFAULT-MODULES). Repeat until no eligible schematic fits. Any remaining threat carries over to the next normal wave. A longer gap results in a larger wave. +- REQ-WAV-SHIP-LEVEL: Each wave's (normal and boss) enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER). Per-ship stats are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS). Threat cost is level-independent (REQ-MOD-THREAT). - REQ-WAV-BOSS-COUNTDOWN: A **boss countdown** timer starts at `world.toml [waves].boss_countdown_seconds` (default 300) at game start and counts down continuously in real game-time seconds. It is not paused during quiet windows. When it reaches 0, a boss wave is triggered (REQ-WAV-BOSS-TRIGGER). Immediately after the boss wave fires, `x` increments (REQ-WAV-BOSS-COUNTER) and a fresh countdown starts at the same configured value. - REQ-WAV-BOSS-ADVANCE: When the player destroys a set of enemy defence stations, the boss countdown is reduced by `world.toml [push].boss_advance_seconds` (default 60), clamped to a minimum of 0. Threat that would have accumulated during the skipped time is not added. If the countdown reaches 0 by this reduction, the boss wave is triggered immediately. - REQ-WAV-QUIET: A **quiet window** suppresses normal wave spawning around each boss wave. The pre-boss quiet window begins when the boss countdown falls to or below `world.toml [waves].boss_quiet_before_seconds` and ends when the countdown reaches 0. The post-boss quiet window begins immediately when the boss wave fires and lasts `world.toml [waves].boss_quiet_after_seconds` seconds. Threat accumulation is paused during both windows. The normal wave gap timer does not advance during either window (REQ-WAV-GAP). The new boss countdown runs during the post-boss quiet window. @@ -400,7 +413,7 @@ The screen is divided into three vertical sections: - REQ-UI-CONFIG-INLINE: Recipe, schematic, ship stance, and target priority configuration for a selected building is shown and changed inline within this panel. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic dropdown (REQ-MOD-UI-PREVIEW). - REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. - REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking empty world space (no building, ship, or defence station) clears the selection. -- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed at the ship's actual level with its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. +- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed at the ship's actual level with its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT). - REQ-UI-STATION-STATS-PANEL: When a single defence station is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate. ### Build Button Grid diff --git a/src/lib/config/ConfigLoader.cpp b/src/lib/config/ConfigLoader.cpp index 9454ddc..d357a09 100644 --- a/src/lib/config/ConfigLoader.cpp +++ b/src/lib/config/ConfigLoader.cpp @@ -429,14 +429,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path) bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds"); } - // Threat - { - const std::string tPath = elemPath + ".threat"; - const toml::table& tTable = requireTable(mt["threat"], file, tPath); - toml::table& tMt = const_cast(tTable); - def.threat.costFormula = requireFormula(tMt["cost_formula"], file, tPath + ".cost_formula"); - } - // Health { const std::string hPath = elemPath + ".health"; @@ -587,7 +579,6 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path) mt["player_production_level"], file, elemPath + ".player_production_level")); def.productionTimeSeconds = requireDouble( mt["production_time_seconds"], file, elemPath + ".production_time_seconds"); - def.threatCost = requireDouble(mt["threat_cost"], file, elemPath + ".threat_cost"); def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color"); def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph"); @@ -704,5 +695,6 @@ GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir) cfg.ships = loadShips(configDir + "/ships.toml"); cfg.stations = loadStations(configDir + "/stations.toml"); cfg.modules = loadModules(configDir + "/modules.toml"); + cfg.threatCosts = computeThreatCostTable(cfg); return cfg; } diff --git a/src/lib/config/GameConfig.h b/src/lib/config/GameConfig.h index 1226b58..ff6b8cb 100644 --- a/src/lib/config/GameConfig.h +++ b/src/lib/config/GameConfig.h @@ -6,6 +6,7 @@ #include "ShipsConfig.h" #include "StationsConfig.h" #include "ModulesConfig.h" +#include "ThreatCostCalculator.h" // Aggregate of all simulation config files. Loaded at startup and reloaded // from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading". @@ -17,4 +18,5 @@ struct GameConfig ShipsConfig ships; StationsConfig stations; ModulesConfig modules; + ThreatCostTable threatCosts; }; diff --git a/src/lib/config/ModulesConfig.h b/src/lib/config/ModulesConfig.h index 100b30a..bfbbcf0 100644 --- a/src/lib/config/ModulesConfig.h +++ b/src/lib/config/ModulesConfig.h @@ -45,7 +45,6 @@ struct ModuleDef std::vector materials; int playerProductionLevel; double productionTimeSeconds; - double threatCost; std::string fillColor; std::string glyph; std::vector statModifiers; diff --git a/src/lib/config/ShipsConfig.h b/src/lib/config/ShipsConfig.h index 28d9538..2dc1425 100644 --- a/src/lib/config/ShipsConfig.h +++ b/src/lib/config/ShipsConfig.h @@ -16,13 +16,6 @@ struct ShipSchematic double productionTimeSeconds; }; -// Wave scheduling cost (REQ-WAV-THREAT-COST). Ships with cost_formula that -// always evaluates to 0 are ineligible as wave picks. -struct ShipThreat -{ - Formula costFormula; -}; - struct ShipHealth { Formula hpFormula; // REQ-SHP-STATS @@ -55,7 +48,6 @@ struct ShipDef std::vector layout; ShipSchematic schematic; - ShipThreat threat; ShipHealth health; ShipMovement movement; ShipSensor sensor; diff --git a/src/lib/eventsystem/event/CMakeLists.txt b/src/lib/eventsystem/event/CMakeLists.txt index 41165c0..b318bef 100644 --- a/src/lib/eventsystem/event/CMakeLists.txt +++ b/src/lib/eventsystem/event/CMakeLists.txt @@ -24,6 +24,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/ArenaStartRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/ArenaInspectRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/WeaponFiredEvent.h + ${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h PARENT_SCOPE ) diff --git a/src/lib/eventsystem/event/DebugDrawToggledEvent.h b/src/lib/eventsystem/event/DebugDrawToggledEvent.h new file mode 100644 index 0000000..839b686 --- /dev/null +++ b/src/lib/eventsystem/event/DebugDrawToggledEvent.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Event.h" + +class DebugDrawToggledEvent : public Event +{ +public: + explicit DebugDrawToggledEvent(bool active) + : active(active) + { + } + + const bool active; +}; diff --git a/src/lib/sim/CMakeLists.txt b/src/lib/sim/CMakeLists.txt index 53af5b6..b2c6d2d 100644 --- a/src/lib/sim/CMakeLists.txt +++ b/src/lib/sim/CMakeLists.txt @@ -9,6 +9,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h + ${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h PARENT_SCOPE ) @@ -21,6 +22,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp PARENT_SCOPE ) diff --git a/src/lib/sim/ThreatCostCalculator.cpp b/src/lib/sim/ThreatCostCalculator.cpp new file mode 100644 index 0000000..2840878 --- /dev/null +++ b/src/lib/sim/ThreatCostCalculator.cpp @@ -0,0 +1,247 @@ +#include "ThreatCostCalculator.h" + +#include +#include + +#include "GameConfig.h" + +namespace +{ + +struct RecipeRef +{ + const RecipeDef* recipe; + std::string outputItem; + int outputAmount; + double probability; +}; + +double computeMaterialThreat(const ThreatCostTable& table, + const std::vector& materials) +{ + double total = 0.0; + for (const RecipeIngredient& mat : materials) + { + std::map::const_iterator it = table.itemThreat.find(mat.item); + if (it != table.itemThreat.end()) + { + total += it->second * mat.amount; + } + } + return total; +} + +bool allInputsResolved(const RecipeDef& recipe, + const std::map& resolved) +{ + for (const RecipeIngredient& input : recipe.inputs) + { + if (resolved.find(input.item) == resolved.end()) + { + return false; + } + } + return true; +} + +double computeRecipeThreat(const RecipeDef& recipe, + const std::map& resolved) +{ + double threat = recipe.durationSeconds; + for (const RecipeIngredient& input : recipe.inputs) + { + threat += resolved.at(input.item) * input.amount; + } + return threat; +} + +} // namespace + + +ThreatCostTable computeThreatCostTable(const GameConfig& config) +{ + ThreatCostTable table; + + // Build lookup: output item → non-reprocessing recipes and reprocessing recipes. + std::map> nonReprocessingRecipes; + std::map> reprocessingRecipes; + + for (const RecipeDef& recipe : config.recipes.recipes) + { + if (recipe.building == BuildingType::ReprocessingPlant) + { + for (const RecipeOutput& out : recipe.outputs) + { + RecipeRef ref; + ref.recipe = &recipe; + ref.outputItem = out.item; + ref.outputAmount = out.amount; + ref.probability = out.probability.value_or(1.0); + reprocessingRecipes[out.item].push_back(ref); + } + } + else + { + for (const RecipeOutput& out : recipe.outputs) + { + RecipeRef ref; + ref.recipe = &recipe; + ref.outputItem = out.item; + ref.outputAmount = out.amount; + ref.probability = 1.0; + nonReprocessingRecipes[out.item].push_back(ref); + } + } + } + + // Collect all item names that need resolving. + std::set unresolved; + for (const std::pair>& entry : nonReprocessingRecipes) + { + unresolved.insert(entry.first); + } + for (const std::pair>& entry : reprocessingRecipes) + { + unresolved.insert(entry.first); + } + + // Iteratively resolve non-reprocessing items. + bool progress = true; + while (progress) + { + progress = false; + std::set newlyResolved; + for (const std::string& item : unresolved) + { + std::map>::const_iterator it = + nonReprocessingRecipes.find(item); + if (it == nonReprocessingRecipes.end()) + { + continue; + } + + double maxThreat = -1.0; + for (const RecipeRef& ref : it->second) + { + if (allInputsResolved(*ref.recipe, table.itemThreat)) + { + double threat = computeRecipeThreat(*ref.recipe, table.itemThreat); + if (threat > maxThreat) + { + maxThreat = threat; + } + } + } + + if (maxThreat >= 0.0) + { + table.itemThreat[item] = maxThreat; + newlyResolved.insert(item); + progress = true; + } + } + for (const std::string& item : newlyResolved) + { + unresolved.erase(item); + } + } + + // Compute scrap threat (REQ-THREAT-SCRAP): find the ship with the smallest + // scrap_drop and use its threat cost. + int minScrapDrop = std::numeric_limits::max(); + const ShipDef* cheapestScrapShip = nullptr; + for (const ShipDef& def : config.ships.ships) + { + if (def.loot.scrapDrop > 0 && def.loot.scrapDrop < minScrapDrop) + { + minScrapDrop = def.loot.scrapDrop; + cheapestScrapShip = &def; + } + } + + if (cheapestScrapShip != nullptr) + { + double shipThreat = calculateShipThreatCost(table, config, + cheapestScrapShip->id, cheapestScrapShip->defaultModules); + table.scrapThreat = shipThreat / minScrapDrop; + } + + // Resolve reprocessing-only items. + for (const std::string& item : unresolved) + { + std::map>::const_iterator it = + reprocessingRecipes.find(item); + if (it == reprocessingRecipes.end()) + { + continue; + } + + for (const RecipeRef& ref : it->second) + { + int scrapPerCycle = 0; + for (const RecipeIngredient& input : ref.recipe->inputs) + { + scrapPerCycle += input.amount; + } + + double threat = (table.scrapThreat * scrapPerCycle + + ref.recipe->durationSeconds) / ref.probability; + std::map::iterator existing = table.itemThreat.find(item); + if (existing == table.itemThreat.end() || threat > existing->second) + { + table.itemThreat[item] = threat; + } + } + } + + return table; +} + + +double calculateShipThreatCost(const ThreatCostTable& table, + const GameConfig& config, + const std::string& shipId, + const std::vector& modules) +{ + const ShipDef* shipDef = nullptr; + for (const ShipDef& d : config.ships.ships) + { + if (d.id == shipId) + { + shipDef = &d; + break; + } + } + if (shipDef == nullptr) + { + return 0.0; + } + + double threat = shipDef->schematic.productionTimeSeconds; + + // Add material threat for ship base materials. + threat += computeMaterialThreat(table, shipDef->schematic.materials); + + // Add module production times and material threats. + for (const PlacedModule& pm : modules) + { + const ModuleDef* moduleDef = nullptr; + for (const ModuleDef& d : config.modules.modules) + { + if (d.id == pm.moduleId) + { + moduleDef = &d; + break; + } + } + if (moduleDef == nullptr) + { + continue; + } + + threat += moduleDef->productionTimeSeconds; + threat += computeMaterialThreat(table, moduleDef->materials); + } + + return threat; +} diff --git a/src/lib/sim/ThreatCostCalculator.h b/src/lib/sim/ThreatCostCalculator.h new file mode 100644 index 0000000..ecf63e2 --- /dev/null +++ b/src/lib/sim/ThreatCostCalculator.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +#include "ShipLayout.h" + +struct GameConfig; + +struct ThreatCostTable +{ + std::map itemThreat; + double scrapThreat = 0.0; +}; + +ThreatCostTable computeThreatCostTable(const GameConfig& config); + +double calculateShipThreatCost(const ThreatCostTable& table, + const GameConfig& config, + const std::string& shipId, + const std::vector& modules); diff --git a/src/lib/sim/WaveSystem.cpp b/src/lib/sim/WaveSystem.cpp index b108652..f2a6c1c 100644 --- a/src/lib/sim/WaveSystem.cpp +++ b/src/lib/sim/WaveSystem.cpp @@ -3,6 +3,7 @@ #include #include "ShipSystem.h" +#include "ThreatCostCalculator.h" #include "tracing.h" WaveSystem::WaveSystem(const GameConfig& config, std::mt19937& rng) @@ -177,7 +178,8 @@ std::vector WaveSystem::selectWaveShips(double& budget, std::vector eligible; for (const ShipDef& def : m_config.ships.ships) { - const double cost = def.threat.costFormula.evaluate(static_cast(shipLevel)); + const double cost = calculateShipThreatCost(m_config.threatCosts, m_config, + def.id, def.defaultModules); if (cost > 0.0) { EligibleShip es; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 599026c..79c0bdd 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -19,5 +19,6 @@ add_files( BlueprintSerializerTest.cpp ModuleConfigTest.cpp ShipModuleTest.cpp + ThreatCostCalculatorTest.cpp RecipeSchematicTest.cpp ) diff --git a/src/test/ModuleConfigTest.cpp b/src/test/ModuleConfigTest.cpp index e7f6885..091619a 100644 --- a/src/test/ModuleConfigTest.cpp +++ b/src/test/ModuleConfigTest.cpp @@ -40,7 +40,6 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]") CHECK(armor.materials[0].amount == 2); CHECK(armor.playerProductionLevel == 1); CHECK(armor.productionTimeSeconds == Approx(3.0)); - CHECK(armor.threatCost == Approx(2.0)); CHECK(armor.fillColor == "#808080"); CHECK(armor.glyph == "A"); REQUIRE(armor.statModifiers.size() == 1); diff --git a/src/test/ThreatCostCalculatorTest.cpp b/src/test/ThreatCostCalculatorTest.cpp new file mode 100644 index 0000000..5cc19a7 --- /dev/null +++ b/src/test/ThreatCostCalculatorTest.cpp @@ -0,0 +1,110 @@ +#include "catch.hpp" + +#include "ConfigLoader.h" +#include "ThreatCostCalculator.h" + +static GameConfig loadConfig() +{ + return ConfigLoader::loadFromDirectory(CONFIG_DIR); +} + +TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + CHECK(table.itemThreat.at("iron_ore") == Approx(1.0)); + CHECK(table.itemThreat.at("copper_ore") == Approx(1.5)); +} + +TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + // iron_ingot: duration 2.0 + iron_ore(1.0) * 2 = 4.0 + CHECK(table.itemThreat.at("iron_ingot") == Approx(4.0)); + // copper_ingot: duration 2.5 + copper_ore(1.5) * 2 = 5.5 + CHECK(table.itemThreat.at("copper_ingot") == Approx(5.5)); +} + +TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + // circuit_board has three non-reprocessing recipes: + // 5.0 + iron_ingot(4.0)*3 + copper_ingot(5.5)*2 = 28.0 + // 3.0 + copper_ingot(5.5)*3 = 19.5 + // 6.0 + iron_ingot(4.0)*5 = 26.0 + // max = 28.0 + CHECK(table.itemThreat.at("circuit_board") == Approx(28.0)); +} + +TEST_CASE("ThreatCostCalculator: scrap threat from cheapest ship", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + // Cheapest ship by scrap_drop is interceptor (scrap_drop=2). + // Interceptor threat: 10 + iron_ingot(4)*3 + circuit_board(28)*1 + // + laser_cannon(5 + iron_ingot(4)*1) = 10 + 12 + 28 + 9 = 59.0 + // scrapThreat = 59.0 / 2 = 29.5 + CHECK(table.scrapThreat == Approx(29.5)); +} + +TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + // advanced_alloy: reprocessing recipe with scrap*5, duration 3.0, probability 0.1 + // (29.5 * 5 + 3.0) / 0.1 = 1505.0 + CHECK(table.itemThreat.at("advanced_alloy") == Approx(1505.0)); +} + +TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + // interceptor: 10 + iron_ingot(4)*3 + circuit_board(28)*1 + laser_cannon(5 + 4*1) = 59.0 + double interceptorThreat = calculateShipThreatCost( + table, cfg, "interceptor", cfg.ships.ships[0].defaultModules); + CHECK(interceptorThreat == Approx(59.0)); + + // salvage_ship (no default modules): 10 + iron_ingot(4)*4 = 26.0 + double salvageThreat = calculateShipThreatCost( + table, cfg, "salvage_ship", cfg.ships.ships[2].defaultModules); + CHECK(salvageThreat == Approx(26.0)); +} + +TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + // interceptor base: 10 + iron_ingot(4)*3 + circuit_board(28)*1 = 50.0 + // + armor_plate: 3 + iron_ingot(4)*2 = 11.0 + // + sensor_booster: 2 + circuit_board(28)*1 = 30.0 + // total = 50.0 + 11.0 + 30.0 = 91.0 + std::vector modules; + PlacedModule armor; + armor.moduleId = "armor_plate"; + modules.push_back(armor); + PlacedModule sensor; + sensor.moduleId = "sensor_booster"; + modules.push_back(sensor); + + double threat = calculateShipThreatCost(table, cfg, "interceptor", modules); + CHECK(threat == Approx(91.0)); +} + +TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]") +{ + const GameConfig cfg = loadConfig(); + const ThreatCostTable& table = cfg.threatCosts; + + double threat = calculateShipThreatCost(table, cfg, "nonexistent_ship", {}); + CHECK(threat == Approx(0.0)); +} diff --git a/src/test/WaveSystemTest.cpp b/src/test/WaveSystemTest.cpp index ce90ca6..da48037 100644 --- a/src/test/WaveSystemTest.cpp +++ b/src/test/WaveSystemTest.cpp @@ -22,6 +22,7 @@ #include "ShipsConfig.h" #include "Simulation.h" #include "Tick.h" +#include "ThreatCostCalculator.h" #include "WaveSystem.h" static GameConfig loadConfig() @@ -201,25 +202,16 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave] REQUIRE(foundEnemyShip); } -TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]") +TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]") { - Simulation sim(loadConfig(), 42); + const GameConfig cfg = loadConfig(); - // Run long enough for several waves. - const int limit = static_cast(secondsToTicks(120.0)); - for (int i = 0; i < limit; ++i) + for (const ShipDef& def : cfg.ships.ships) { - sim.tick(); + const double cost = calculateShipThreatCost(cfg.threatCosts, cfg, + def.id, def.defaultModules); + CHECK(cost > 0.0); } - - sim.admin().forEach( - [&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f) - { - if (!f.isEnemy) { return; } - // salvage_ship and repair_ship have cost_formula = "0" and must not spawn. - REQUIRE(si.schematicId != "salvage_ship"); - REQUIRE(si.schematicId != "repair_ship"); - }); } // --------------------------------------------------------------------------- diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 485b7a1..973a24f 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -1144,6 +1144,8 @@ void GameWorldView::keyPressEvent(QKeyEvent* event) break; case Qt::Key_M: m_debugDraw = !m_debugDraw; + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_debugDraw)); break; case Qt::Key_L: EventManager::getInstance()->addEvent(std::make_shared()); @@ -1438,6 +1440,11 @@ double GameWorldView::gameSpeed() const return m_gameSpeedMultiplier; } +bool GameWorldView::isDebugDrawEnabled() const +{ + return m_debugDraw; +} + void GameWorldView::resetFrameTimer() { m_frameTimer.restart(); diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index c321ac9..7c83e9b 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -24,6 +24,7 @@ #include "EventHandler.h" #include "ExitBlueprintModeRequestedEvent.h" #include "ExitBuilderModeRequestedEvent.h" +#include "DebugDrawToggledEvent.h" #include "WeaponFiredEvent.h" #include "SchematicChoiceOption.h" #include "SpeedChangeRequestedEvent.h" @@ -65,6 +66,7 @@ public: ~GameWorldView() override; double gameSpeed() const; + bool isDebugDrawEnabled() const; void resetFrameTimer(); void setGameSpeed(double multiplier); void resetForNewGame(); diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 7173aa3..815dcb2 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -223,6 +223,7 @@ void MainWindow::handleEvent(std::shared_ptr e m_layoutBlueprints, std::move(unlockedModuleIds), std::move(moduleLevels), + m_gameWorldView->isDebugDrawEnabled(), this); if (dialog.exec() == QDialog::Accepted && dialog.result().has_value()) { diff --git a/src/ui/SelectedBuildingPanel.cpp b/src/ui/SelectedBuildingPanel.cpp index cc91422..a806292 100644 --- a/src/ui/SelectedBuildingPanel.cpp +++ b/src/ui/SelectedBuildingPanel.cpp @@ -23,6 +23,7 @@ #include "ShipIdentityComponent.h" #include "ShipStatsCalculator.h" #include "ShipStatsPanel.h" +#include "ThreatCostCalculator.h" #include "StationBodyComponent.h" #include "TickAdvancedEvent.h" #include "Building.h" @@ -784,6 +785,19 @@ void SelectedBuildingPanel::buildEntityShip(entt::entity entity) const ShipStats stats = buildShipStatsFromEntity(admin, entity); m_entityStatsPanel->refreshFromLive(stats, health.hp); + m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw); + + for (const ShipDef& def : m_config->ships.ships) + { + if (def.id == identity.schematicId) + { + double threat = calculateShipThreatCost( + m_config->threatCosts, *m_config, def.id, def.defaultModules); + m_entityStatsPanel->setThreatCost(threat); + break; + } + } + m_entityStatsPanel->show(); m_stationStatsLabel->hide(); @@ -869,3 +883,9 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptrids); } + +void SelectedBuildingPanel::handleEvent(std::shared_ptr event) +{ + m_debugDraw = event->active; + m_entityStatsPanel->setDebugDrawEnabled(event->active); +} diff --git a/src/ui/SelectedBuildingPanel.h b/src/ui/SelectedBuildingPanel.h index 5eecfe3..ad460df 100644 --- a/src/ui/SelectedBuildingPanel.h +++ b/src/ui/SelectedBuildingPanel.h @@ -11,6 +11,7 @@ #include "Building.h" #include "BuildingId.h" +#include "DebugDrawToggledEvent.h" #include "EntitySelectedEvent.h" #include "EventHandler.h" #include "GameConfig.h" @@ -33,7 +34,8 @@ class QVBoxLayout; class SelectedBuildingPanel : public QWidget, public CombinedEventHandler + SelectionChangedEvent, + DebugDrawToggledEvent> { Q_OBJECT @@ -46,6 +48,7 @@ private: void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; + void handleEvent(std::shared_ptr event) override; private slots: void onRecipeChanged(int comboIndex); @@ -86,6 +89,7 @@ private: QPoint m_splitterTile; std::string m_currentRecipeId; + bool m_debugDraw = false; std::optional m_selectedEntity; ShipStatsPanel* m_entityStatsPanel; QLabel* m_entityTitleLabel; diff --git a/src/ui/ShipLayoutDialog.cpp b/src/ui/ShipLayoutDialog.cpp index 56bcb8d..e29b98d 100644 --- a/src/ui/ShipLayoutDialog.cpp +++ b/src/ui/ShipLayoutDialog.cpp @@ -366,6 +366,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, std::vector& allBlueprints, std::set unlockedModuleIds, std::map moduleLevels, + bool debugDraw, QWidget* parent) : QDialog(parent) , m_config(config) @@ -380,6 +381,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, , m_removeButton(nullptr) , m_gridWidget(nullptr) , m_statsPanel(nullptr) + , m_debugDraw(debugDraw) { setWindowTitle(tr("Configure Ship Layout")); setModal(true); @@ -434,6 +436,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, // Left column: ship stats panel. m_statsPanel = new ShipStatsPanel(config, this); + m_statsPanel->setDebugDrawEnabled(m_debugDraw); columnsLayout->addWidget(m_statsPanel); // Center column: module selection buttons. diff --git a/src/ui/ShipLayoutDialog.h b/src/ui/ShipLayoutDialog.h index 598513c..6bbf477 100644 --- a/src/ui/ShipLayoutDialog.h +++ b/src/ui/ShipLayoutDialog.h @@ -28,6 +28,7 @@ public: std::vector& allBlueprints, std::set unlockedModuleIds, std::map moduleLevels, + bool debugDraw, QWidget* parent = nullptr); std::optional result() const; @@ -77,6 +78,7 @@ private: QPushButton* m_removeButton; QWidget* m_gridWidget; ShipStatsPanel* m_statsPanel; + bool m_debugDraw; std::optional m_result; }; diff --git a/src/ui/ShipStatsPanel.cpp b/src/ui/ShipStatsPanel.cpp index 21ed5c3..d581291 100644 --- a/src/ui/ShipStatsPanel.cpp +++ b/src/ui/ShipStatsPanel.cpp @@ -6,6 +6,7 @@ #include "GameConfig.h" #include "ShipStatsCalculator.h" +#include "ThreatCostCalculator.h" namespace { @@ -103,6 +104,11 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent) m_repairSection->setVisible(false); layout->addWidget(m_repairSection); + // Threat cost — debug-only, initially hidden. + m_threatCostLabel = makeStatLabel(this); + m_threatCostLabel->setVisible(false); + layout->addWidget(m_threatCostLabel); + layout->addStretch(); } @@ -115,6 +121,10 @@ void ShipStatsPanel::refresh(const std::string& shipId, moduleLevelOverrides); const QString hpText = tr("HP: %1").arg(static_cast(stats.hp + 0.5f)); applyStats(stats, hpText); + + const double threat = calculateShipThreatCost(m_config->threatCosts, *m_config, + shipId, modules); + setThreatCost(threat); } void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp) @@ -180,3 +190,15 @@ void ShipStatsPanel::applyStats(const ShipStats& stats, const QString& hpText) m_repairSection->setVisible(false); } } + +void ShipStatsPanel::setThreatCost(double cost) +{ + m_threatCostLabel->setText(tr("Threat Cost: %1").arg(cost, 0, 'f', 1)); + m_threatCostLabel->setVisible(m_debugDraw); +} + +void ShipStatsPanel::setDebugDrawEnabled(bool enabled) +{ + m_debugDraw = enabled; + m_threatCostLabel->setVisible(m_debugDraw); +} diff --git a/src/ui/ShipStatsPanel.h b/src/ui/ShipStatsPanel.h index e95cb5b..c23fa72 100644 --- a/src/ui/ShipStatsPanel.h +++ b/src/ui/ShipStatsPanel.h @@ -26,10 +26,14 @@ public: void refreshFromLive(const ShipStats& stats, float currentHp); + void setThreatCost(double cost); + void setDebugDrawEnabled(bool enabled); + private: void applyStats(const ShipStats& stats, const QString& hpText); const GameConfig* m_config; + bool m_debugDraw = false; QLabel* m_hpLabel; QLabel* m_speedLabel; @@ -50,4 +54,6 @@ private: QWidget* m_repairSection; QLabel* m_repairRateLabel; QLabel* m_repairRangeLabel; + + QLabel* m_threatCostLabel; };