Derive ship scrap drop from threat

This commit is contained in:
2026-07-02 21:30:38 +02:00
parent 5bc581bcb8
commit 81b1c7a66b
16 changed files with 61 additions and 113 deletions

View File

@@ -264,6 +264,7 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
cfg.startingBuildingBlocks = static_cast<int>(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<int>(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<toml::table&>(lTable);
def.loot.scrapDrop = static_cast<int>(requireInt(lMt["scrap_drop"], file, lPath + ".scrap_drop"));
}
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
if (mt.contains("default_modules"))
{

View File

@@ -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<PlacedModule> defaultModules;

View File

@@ -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

View File

@@ -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;
};

View File

@@ -1,6 +1,8 @@
#include "ShipSystem.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <map>
#include <stdexcept>
#include <utility>
@@ -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<PlacedModule>& 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<int>(std::lround(threatCost * m_config.world.scrapPerThreat)))
: 0;
m_admin.get<ShipIdentityComponent>(entity).scrapDrop = scrapDrop;
// --- Pass 1: create capability child entities ----------------------------
std::vector<entt::entity> weaponChildren;
std::vector<entt::entity> salvageChildren;

View File

@@ -551,15 +551,11 @@ void Simulation::tickDeathsAndLoot()
{
const ShipIdentityComponent& si = m_admin.get<ShipIdentityComponent>(deadEntity);
const PositionComponent& pos = m_admin.get<PositionComponent>(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);
}

View File

@@ -1,6 +1,5 @@
#include "ThreatCostCalculator.h"
#include <limits>
#include <set>
#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<std::string, std::vector<RecipeRef>> nonReprocessingRecipes;
std::map<std::string, std::vector<RecipeRef>> 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<int>::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)
{