From 81b1c7a66bbd53092fc5bb708ddf5d8a3206b64d Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Thu, 2 Jul 2026 21:30:38 +0200 Subject: [PATCH] Derive ship scrap drop from threat --- bin/app/data/config/ships.toml | 24 --------------- bin/app/data/config/world.toml | 1 + bin/test/data/config/ships.toml | 12 -------- bin/test/data/config/world.toml | 1 + docs/requirements.md | 8 ++--- src/balancing/ArenaSimulation.cpp | 12 +++----- src/lib/config/ConfigLoader.cpp | 9 +----- src/lib/config/ShipsConfig.h | 7 ----- src/lib/config/WorldConfig.h | 1 + src/lib/ecs/component/ShipIdentityComponent.h | 3 ++ src/lib/ecs/system/ShipSystem.cpp | 14 +++++++++ src/lib/sim/Simulation.cpp | 12 +++----- src/lib/sim/ThreatCostCalculator.cpp | 29 +++++-------------- src/test/CombatSystemTest.cpp | 22 ++++++-------- src/test/ConfigLoaderTest.cpp | 4 +++ src/test/ThreatCostCalculatorTest.cpp | 15 +++++----- 16 files changed, 61 insertions(+), 113 deletions(-) diff --git a/bin/app/data/config/ships.toml b/bin/app/data/config/ships.toml index a3be7af..754d470 100644 --- a/bin/app/data/config/ships.toml +++ b/bin/app/data/config/ships.toml @@ -39,9 +39,6 @@ max_rotation_speed_radps_formula = "6.28" [ship.sensor] sensor_range_m_formula = "150" -[ship.loot] -scrap_drop = 2 - # Frigate — 5 cells in a plus shape. Holds a couple of small guns plus at # most one 1x2 support (every 1x2 placement crosses the center cell), or one @@ -73,9 +70,6 @@ max_rotation_speed_radps_formula = "4" [ship.sensor] sensor_range_m_formula = "200" -[ship.loot] -scrap_drop = 5 - # Destroyer — 8 cells: a long gun deck with three turret bumps on top. # Still no 2x2 area, so it packs more small guns than a frigate but can never @@ -106,9 +100,6 @@ max_rotation_speed_radps_formula = "3" [ship.sensor] sensor_range_m_formula = "220" -[ship.loot] -scrap_drop = 8 - # Cruiser — 12 cells with notched corners. Fits at most two 2x2 m guns # (stacked through the middle), leaving the four side cells for small @@ -141,9 +132,6 @@ max_rotation_speed_radps_formula = "2" [ship.sensor] sensor_range_m_formula = "250" -[ship.loot] -scrap_drop = 15 - # Battlecruiser — 16 cells: a wide bow split into two gun cheeks, tapering # toward the stern. Fits three 2x2 m guns (two in the cheeks, one through @@ -177,9 +165,6 @@ max_rotation_speed_radps_formula = "1.5" [ship.sensor] sensor_range_m_formula = "260" -[ship.loot] -scrap_drop = 20 - # Battleship — 24 cells: a broadside hull with notched flanks on every other # row. Fits four 2x2 m guns (two per gun deck) with the bow, stern, and flank @@ -216,9 +201,6 @@ max_rotation_speed_radps_formula = "1" [ship.sensor] sensor_range_m_formula = "280" -[ship.loot] -scrap_drop = 35 - # Dreadnought — 36 cells: the main battery deck is split into three 3x3 gun # slots by structural spacer columns, so exactly three l guns fit side by @@ -254,9 +236,6 @@ max_rotation_speed_radps_formula = "0.5" [ship.sensor] sensor_range_m_formula = "300" -[ship.loot] -scrap_drop = 60 - # Carrier — 37 cells: the top flight deck (rows 0-1) is the only place wide # enough for the 2x6 drone hangar, and exactly one fits. The middle deck row @@ -290,6 +269,3 @@ max_rotation_speed_radps_formula = "0.5" [ship.sensor] sensor_range_m_formula = "350" - -[ship.loot] -scrap_drop = 60 diff --git a/bin/app/data/config/world.toml b/bin/app/data/config/world.toml index 8ba6195..a9df991 100644 --- a/bin/app/data/config/world.toml +++ b/bin/app/data/config/world.toml @@ -3,6 +3,7 @@ height_tiles = 40 refund_percentage = 100 starting_building_blocks = 1000 scrap_despawn_seconds = 30 +scrap_per_threat = 0.01 tile_size_m = 10 belt_speed_mps = 20 tunnel_max_distance_tiles = 10 diff --git a/bin/test/data/config/ships.toml b/bin/test/data/config/ships.toml index f0b6551..4a9c14d 100644 --- a/bin/test/data/config/ships.toml +++ b/bin/test/data/config/ships.toml @@ -22,9 +22,6 @@ max_rotation_speed_radps_formula = "100000" [ship.sensor] sensor_range_m_formula = "2000" -[ship.loot] -scrap_drop = 2 - [[ship]] id = "destroyer" @@ -50,9 +47,6 @@ max_rotation_speed_radps_formula = "100000" [ship.sensor] sensor_range_m_formula = "3000" -[ship.loot] -scrap_drop = 4 - [[ship]] id = "salvage_ship" @@ -77,9 +71,6 @@ max_rotation_speed_radps_formula = "100000" [ship.sensor] sensor_range_m_formula = "2500" -[ship.loot] -scrap_drop = 2 - [[ship]] id = "repair_ship" @@ -103,6 +94,3 @@ max_rotation_speed_radps_formula = "100000" [ship.sensor] sensor_range_m_formula = "2500" - -[ship.loot] -scrap_drop = 2 diff --git a/bin/test/data/config/world.toml b/bin/test/data/config/world.toml index 811f44d..9cec468 100644 --- a/bin/test/data/config/world.toml +++ b/bin/test/data/config/world.toml @@ -3,6 +3,7 @@ height_tiles = 60 refund_percentage = 75 starting_building_blocks = 100 scrap_despawn_seconds = 30 +scrap_per_threat = 1.0 tile_size_m = 10 belt_speed_mps = 20 tunnel_max_distance_tiles = 10 diff --git a/docs/requirements.md b/docs/requirements.md index 73c0312..525a4df 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -4,10 +4,10 @@ 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, enemy ship level formula, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, and artifact win count. +- **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, 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, and artifact win count. - **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, 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). +- **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, 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; 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. @@ -149,7 +149,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des ## Resources -- REQ-RES-SCRAP-DROP: Destroyed ships (both player and enemy) and destroyed defence stations (both player and enemy) drop scrap at their location. The scrap amount per ship is defined in `ships.toml [ship.loot].scrap_drop`; for stations it is defined as `stations.toml [player_station].scrap_drop_formula` and `[enemy_station].scrap_drop_formula`. A scrap drop carries an amount; salvage modules collect it one scrap per cycle (REQ-SHP-SALVAGE), and the drop is removed from the world once its remaining amount reaches zero or `world.toml [world].scrap_despawn_seconds` seconds have elapsed since it was dropped, whichever comes first. +- REQ-RES-SCRAP-DROP: Destroyed ships (both player and enemy) and destroyed defence stations (both player and enemy) drop scrap at their location. The scrap amount per ship is derived from the ship's threat cost (REQ-MOD-THREAT) for its as-built layout, multiplied by `world.toml [world].scrap_per_threat` (default 0.01) and rounded to the nearest integer (at least 1 for any ship whose threat cost is greater than 0); for stations it is defined as `stations.toml [player_station].scrap_drop_formula` and `[enemy_station].scrap_drop_formula`. A scrap drop carries an amount; salvage modules collect it one scrap per cycle (REQ-SHP-SALVAGE), and the drop is removed from the world once its remaining amount reaches zero or `world.toml [world].scrap_despawn_seconds` seconds have elapsed since it was dropped, whichever comes first. - REQ-RES-SCRAP-COLLECT: Scrap is collected by salvage ships and delivered to a Salvage Bay on the asteroid. From there it can be fed via belt into a smelter (same output as ore) or a Reprocessing Plant. ## Ships @@ -231,7 +231,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - **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-THREAT-SCRAP: The threat value of scrap is the constant `1 / world.toml [world].scrap_per_threat`. This is the exact inverse of the scrap-drop conversion in REQ-RES-SCRAP-DROP, so a destroyed ship drops scrap worth precisely its own threat cost. Because scrap threat is now a fixed constant, it no longer depends on any ship's threat cost, removing the potential circularity with REQ-MOD-THREAT for ships built from reprocessing-only materials. - 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`. diff --git a/src/balancing/ArenaSimulation.cpp b/src/balancing/ArenaSimulation.cpp index c886e51..8bbde12 100644 --- a/src/balancing/ArenaSimulation.cpp +++ b/src/balancing/ArenaSimulation.cpp @@ -330,15 +330,11 @@ void ArenaSimulation::tickDeaths() { const ShipIdentityComponent& si = m_admin.get(deadEntity); const PositionComponent& pos = m_admin.get(deadEntity); - for (const ShipDef& def : m_gameConfig.ships.ships) + if (si.scrapDrop > 0) { - if (def.id == si.schematicId && def.loot.scrapDrop > 0) - { - const Tick despawnAt = m_currentTick - + secondsToTicks(m_gameConfig.world.scrapDespawnSeconds); - m_scrapSystem->spawn(pos.value, def.loot.scrapDrop, despawnAt); - break; - } + const Tick despawnAt = m_currentTick + + secondsToTicks(m_gameConfig.world.scrapDespawnSeconds); + m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt); } m_shipSystem->despawn(deadEntity); } diff --git a/src/lib/config/ConfigLoader.cpp b/src/lib/config/ConfigLoader.cpp index 136ad9c..2bc65a2 100644 --- a/src/lib/config/ConfigLoader.cpp +++ b/src/lib/config/ConfigLoader.cpp @@ -264,6 +264,7 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path) cfg.refundPercentage = static_cast(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage")); cfg.startingBuildingBlocks = static_cast(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks")); cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds"); + cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat"); cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m"); cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m; cfg.tunnelMaxDistance_tiles = static_cast(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles")); @@ -466,14 +467,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path) def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_m_formula"], file, snsPath + ".sensor_range_m_formula"); } - // Loot - { - const std::string lPath = elemPath + ".loot"; - const toml::table& lTable = requireTable(mt["loot"], file, lPath); - toml::table& lMt = const_cast(lTable); - def.loot.scrapDrop = static_cast(requireInt(lMt["scrap_drop"], file, lPath + ".scrap_drop")); - } - // Optional: default_modules (REQ-WAV-DEFAULT-MODULES) if (mt.contains("default_modules")) { diff --git a/src/lib/config/ShipsConfig.h b/src/lib/config/ShipsConfig.h index 2dc1425..4d7a3f6 100644 --- a/src/lib/config/ShipsConfig.h +++ b/src/lib/config/ShipsConfig.h @@ -35,12 +35,6 @@ struct ShipSensor Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS }; -// Scrap dropped on destruction (REQ-RES-SCRAP-DROP). -struct ShipLoot -{ - int scrapDrop; -}; - struct ShipDef { std::string id; @@ -51,7 +45,6 @@ struct ShipDef ShipHealth health; ShipMovement movement; ShipSensor sensor; - ShipLoot loot; // Module layout used for enemy wave ships (REQ-WAV-DEFAULT-MODULES). std::vector defaultModules; diff --git a/src/lib/config/WorldConfig.h b/src/lib/config/WorldConfig.h index f0b7491..018a8fb 100644 --- a/src/lib/config/WorldConfig.h +++ b/src/lib/config/WorldConfig.h @@ -60,6 +60,7 @@ struct WorldConfig int refundPercentage; // REQ-BLD-DEMOLISH int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP + double scrapPerThreat; // REQ-RES-SCRAP-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat) double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE) double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config) int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR diff --git a/src/lib/ecs/component/ShipIdentityComponent.h b/src/lib/ecs/component/ShipIdentityComponent.h index 5670b87..9f7df9e 100644 --- a/src/lib/ecs/component/ShipIdentityComponent.h +++ b/src/lib/ecs/component/ShipIdentityComponent.h @@ -6,4 +6,7 @@ struct ShipIdentityComponent { int level; std::string schematicId; + // Scrap dropped on destruction, derived from the ship's as-built threat cost + // at spawn time (REQ-RES-SCRAP-DROP). + int scrapDrop = 0; }; diff --git a/src/lib/ecs/system/ShipSystem.cpp b/src/lib/ecs/system/ShipSystem.cpp index d23b950..7c65d78 100644 --- a/src/lib/ecs/system/ShipSystem.cpp +++ b/src/lib/ecs/system/ShipSystem.cpp @@ -1,6 +1,8 @@ #include "ShipSystem.h" +#include #include +#include #include #include #include @@ -26,7 +28,9 @@ #include "SalvagerComponent.h" #include "SelectedBehaviorComponent.h" #include "SensorRangeComponent.h" +#include "ShipIdentityComponent.h" #include "StandbyBehavior.h" +#include "ThreatCostCalculator.h" #include "Tick.h" #include "tracing.h" #include "WeaponComponent.h" @@ -103,6 +107,16 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, const std::vector& modules = layout.has_value() ? layout->placedModules : def->defaultModules; + // Derive the scrap dropped on destruction from the ship's as-built threat cost + // (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat), floored at 1 for any + // ship with threat > 0. Computed once here since threat is level-independent. + const double threatCost = calculateShipThreatCost(m_config.threatCosts, m_config, + schematicId, modules); + const int scrapDrop = threatCost > 0.0 + ? std::max(1, static_cast(std::lround(threatCost * m_config.world.scrapPerThreat))) + : 0; + m_admin.get(entity).scrapDrop = scrapDrop; + // --- Pass 1: create capability child entities ---------------------------- std::vector weaponChildren; std::vector salvageChildren; diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index 5afda8c..939ddec 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -551,15 +551,11 @@ void Simulation::tickDeathsAndLoot() { const ShipIdentityComponent& si = m_admin.get(deadEntity); const PositionComponent& pos = m_admin.get(deadEntity); - for (const ShipDef& def : m_config.ships.ships) + if (si.scrapDrop > 0) { - if (def.id == si.schematicId && def.loot.scrapDrop > 0) - { - const Tick despawnAt = m_currentTick - + secondsToTicks(m_config.world.scrapDespawnSeconds); - m_scrapSystem->spawn(pos.value, def.loot.scrapDrop, despawnAt); - break; - } + const Tick despawnAt = m_currentTick + + secondsToTicks(m_config.world.scrapDespawnSeconds); + m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt); } m_shipSystem->despawn(deadEntity); } diff --git a/src/lib/sim/ThreatCostCalculator.cpp b/src/lib/sim/ThreatCostCalculator.cpp index 2840878..2ef7c04 100644 --- a/src/lib/sim/ThreatCostCalculator.cpp +++ b/src/lib/sim/ThreatCostCalculator.cpp @@ -1,6 +1,5 @@ #include "ThreatCostCalculator.h" -#include #include #include "GameConfig.h" @@ -62,6 +61,14 @@ ThreatCostTable computeThreatCostTable(const GameConfig& config) { ThreatCostTable table; + // Scrap threat (REQ-THREAT-SCRAP) is the constant inverse of the scrap-drop + // conversion (REQ-RES-SCRAP-DROP): one scrap is worth 1 / scrap_per_threat. + // Set it up front so reprocessing-only item threats (below) can use it, and so + // it no longer depends on any ship's threat cost. + table.scrapThreat = config.world.scrapPerThreat > 0.0 + ? 1.0 / config.world.scrapPerThreat + : 0.0; + // Build lookup: output item → non-reprocessing recipes and reprocessing recipes. std::map> nonReprocessingRecipes; std::map> reprocessingRecipes; @@ -146,26 +153,6 @@ ThreatCostTable computeThreatCostTable(const GameConfig& config) } } - // 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) { diff --git a/src/test/CombatSystemTest.cpp b/src/test/CombatSystemTest.cpp index 8b0b5e3..35c9f49 100644 --- a/src/test/CombatSystemTest.cpp +++ b/src/test/CombatSystemTest.cpp @@ -14,6 +14,7 @@ #include "HealthComponent.h" #include "HqProxyComponent.h" #include "ModuleOwnerComponent.h" +#include "ScrapDataComponent.h" #include "ScrapSystem.h" #include "ShipSystem.h" #include "Simulation.h" @@ -406,24 +407,19 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]") { Simulation sim(loadConfig(), 42); - const ShipDef* droppingDef = nullptr; - for (const ShipDef& def : sim.config().ships.ships) - { - if (def.loot.scrapDrop > 0) - { - droppingDef = &def; - break; - } - } - REQUIRE(droppingDef != nullptr); - - const entt::entity ship = sim.ships().spawn(droppingDef->id, 1, + // Scrap dropped on death is derived from the ship's as-built threat cost + // (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat). The interceptor's + // threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops + // round(59.0 * 1.0) = 59 scrap. + const entt::entity ship = sim.ships().spawn("interceptor", 1, QVector2D(10.0f, 10.0f)); sim.admin().get(ship).hp = -1.0f; sim.tick(); - REQUIRE(!sim.scraps().allScrapInfo().empty()); + const std::vector scraps = sim.scraps().allScrapInfo(); + REQUIRE(scraps.size() == 1); + CHECK(sim.admin().get(scraps[0].entity).amount == 59); } TEST_CASE("CombatSystem: HQ death sets game over", "[combat]") diff --git a/src/test/ConfigLoaderTest.cpp b/src/test/ConfigLoaderTest.cpp index cb3f76b..5b3dccf 100644 --- a/src/test/ConfigLoaderTest.cpp +++ b/src/test/ConfigLoaderTest.cpp @@ -78,6 +78,7 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c REQUIRE(cfg.world.push.bossAdvanceSeconds == Approx(60.0)); REQUIRE(cfg.world.orbitFactor == Approx(0.8)); REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0)); + REQUIRE(cfg.world.scrapPerThreat == Approx(1.0)); // Spot-check that a config-derived formula computes as expected. // threat_rate_formula = "x": evaluates to the input value. @@ -167,6 +168,7 @@ TEST_CASE("Missing field in world.toml is rejected with the field path", "[confi height_tiles = 60 refund_percentage = 75 scrap_despawn_seconds = 30 +scrap_per_threat = 0.01 tile_size_m = 10 belt_speed_mps = 20 starting_building_blocks = 100 @@ -217,6 +219,7 @@ TEST_CASE("Malformed formula in world.toml is rejected with field identification height_tiles = 60 refund_percentage = 75 scrap_despawn_seconds = 30 +scrap_per_threat = 0.01 tile_size_m = 10 belt_speed_mps = 20 starting_building_blocks = 100 @@ -268,6 +271,7 @@ TEST_CASE("Inverted wave gap range is rejected", "[config]") height_tiles = 60 refund_percentage = 75 scrap_despawn_seconds = 30 +scrap_per_threat = 0.01 tile_size_m = 10 belt_speed_mps = 20 diff --git a/src/test/ThreatCostCalculatorTest.cpp b/src/test/ThreatCostCalculatorTest.cpp index 5cc19a7..6fa2f13 100644 --- a/src/test/ThreatCostCalculatorTest.cpp +++ b/src/test/ThreatCostCalculatorTest.cpp @@ -41,16 +41,15 @@ TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]" CHECK(table.itemThreat.at("circuit_board") == Approx(28.0)); } -TEST_CASE("ThreatCostCalculator: scrap threat from cheapest ship", "[threat]") +TEST_CASE("ThreatCostCalculator: scrap threat is 1 / scrap_per_threat", "[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)); + // REQ-THREAT-SCRAP: scrap threat is the constant 1 / world.scrap_per_threat. + // The test config sets scrap_per_threat = 1.0, so scrapThreat = 1.0. + CHECK(table.scrapThreat == Approx(1.0 / cfg.world.scrapPerThreat)); + CHECK(table.scrapThreat == Approx(1.0)); } TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]") @@ -59,8 +58,8 @@ TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]") 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)); + // scrapThreat = 1.0 (= 1 / scrap_per_threat), so (1.0 * 5 + 3.0) / 0.1 = 80.0 + CHECK(table.itemThreat.at("advanced_alloy") == Approx(80.0)); } TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]")