add collection rate for salvager modules and respect collection range of each of these modules

This commit is contained in:
2026-06-02 21:20:44 +02:00
parent 9d0a60a93b
commit f921f00a0d
8 changed files with 64 additions and 24 deletions

View File

@@ -78,6 +78,7 @@ glyph = "Sv"
[module.salvage] [module.salvage]
collection_range_formula = "50" collection_range_formula = "50"
cargo_capacity_formula = "10" cargo_capacity_formula = "10"
collection_rate_formula = "0.5"
[[module]] [[module]]
id = "repair_tool_module" id = "repair_tool_module"

View File

@@ -65,6 +65,7 @@ glyph = "Sv"
[module.salvage] [module.salvage]
collection_range_formula = "50" collection_range_formula = "50"
cargo_capacity_formula = "10" cargo_capacity_formula = "10"
collection_rate_formula = "0.5"
[[module]] [[module]]
id = "repair_tool_module" id = "repair_tool_module"

View File

@@ -8,7 +8,7 @@ Config files use the TOML format. The following config files drive game paramete
- **buildings.toml** — building block cost and construction time per building type. - **buildings.toml** — building block cost and construction time per building type.
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. - **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities.
- **ships.toml** — per schematic: a human-readable display name (used in toasts and 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, whether the schematic is available from game start, 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 toasts and 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, whether the schematic is available from game start, 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, player production level, production time, threat cost, fill color, glyph, 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. 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, player production level, production time, threat cost, fill color, glyph, 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; beam color and 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; 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. - **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.
@@ -161,7 +161,9 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
- Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid). - Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid).
- Target priority: closest / highest HP / structures first. - Target priority: closest / highest HP / structures first.
- REQ-SHP-RALLY: After spawning, aggressive-stance ships with weapon modules move to and loiter at the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position). While at the rally point, ships still engage any enemy that enters sensor range. Every `world.toml [world].departure_interval_seconds` seconds (default 20), all ships with weapon modules currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point. - REQ-SHP-RALLY: After spawning, aggressive-stance ships with weapon modules move to and loiter at the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position). While at the rally point, ships still engage any enemy that enters sensor range. Every `world.toml [world].departure_interval_seconds` seconds (default 20), all ships with weapon modules currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point.
- REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it, collect, and deliver it to a Salvage Bay on the asteroid; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Ships with salvage modules are vulnerable to enemy ships while operating. - REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it; when it is within a module's `collection_range`, that module collects it (consuming the scrap entity). Once all cargo is full, fly to a Salvage Bay and deliver; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Ships with salvage modules are vulnerable to enemy ships while operating.
Each salvage module instance operates independently: it has its own cargo hold (`cargo_capacity`), collection range (`collection_range`), and collection rate (`collection_rate`, in collections per second). After collecting a piece of scrap, the module cannot collect again until `1 / collection_rate` seconds have elapsed. A ship with multiple salvage modules can therefore collect multiple pieces of scrap per tick (one per ready module), and installs of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
- REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard: - REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard:
- Defence stations first / ships first / nearest target. - Defence stations first / ships first / nearest target.
- REQ-SHP-ENEMY-AI: **Enemy ships** — engage the closest valid target (player defence station, HQ, or player ship) within their sensor range. If no target is in sensor range, they move toward the asteroid (leftward in world coordinates). - REQ-SHP-ENEMY-AI: **Enemy ships** — engage the closest valid target (player defence station, HQ, or player ship) within their sensor range. If no target is in sensor range, they move toward the asteroid (leftward in world coordinates).
@@ -180,7 +182,10 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
- `threat_cost` — threat cost added to the ship's threat cost 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. - `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. - `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 (e.g. `damage_formula`, `attack_range_formula`, `attack_rate_formula` for weapons) 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. - 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:
- **Weapon** (`[module.weapon]`): `damage_formula`, `attack_range_formula`, `attack_rate_formula`.
- **Salvage** (`[module.salvage]`): `collection_range_formula` (tiles), `cargo_capacity_formula` (integer scrap units), `collection_rate_formula` (collections per second).
- **Repair** (`[module.repair]`): `repair_rate_formula` (HP/s), `repair_range_formula` (tiles).
- Zero or more **passive stat modifier formulas** (`added_*`/`multiplied_*`) that boost stats on the ship hull or on capability module instances (see REQ-MOD-STAT-CALC). A single module may be both a capability module and provide passive modifiers. - Zero or more **passive stat modifier formulas** (`added_*`/`multiplied_*`) that boost stats on the ship hull or on capability module instances (see REQ-MOD-STAT-CALC). A single module may be both a capability module and provide passive modifiers.
- REQ-MOD-LAYOUT: Each ship in `ships.toml` defines a `layout` — a list of strings representing the ship's module grid (see Ship Layout Format). All ships define a layout. - REQ-MOD-LAYOUT: Each ship in `ships.toml` defines a `layout` — a list of strings representing the ship's module grid (see Ship Layout Format). All ships define a layout.

View File

@@ -530,6 +530,7 @@ static const StatEntry kKnownStats[] = {
{"weapon", "attack_rate"}, {"weapon", "attack_rate"},
{"salvage", "collection_range"}, {"salvage", "collection_range"},
{"salvage", "cargo_capacity"}, {"salvage", "cargo_capacity"},
{"salvage", "collection_rate"},
{"repair", "repair_rate"}, {"repair", "repair_rate"},
{"repair", "repair_range"}, {"repair", "repair_range"},
}; };
@@ -636,13 +637,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const std::string sPath = elemPath + ".salvage"; const std::string sPath = elemPath + ".salvage";
const toml::table& sTable = requireTable(mt["salvage"], file, sPath); const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
toml::table& sMt = const_cast<toml::table&>(sTable); toml::table& sMt = const_cast<toml::table&>(sTable);
if (sMt.contains("collection_range_formula") || sMt.contains("cargo_capacity_formula")) if (sMt.contains("collection_range_formula") || sMt.contains("cargo_capacity_formula")
|| sMt.contains("collection_rate_formula"))
{ {
ModuleSalvageCapability cap; ModuleSalvageCapability cap;
cap.collectionRangeFormula = requireFormula(sMt["collection_range_formula"], cap.collectionRangeFormula = requireFormula(sMt["collection_range_formula"],
file, sPath + ".collection_range_formula"); file, sPath + ".collection_range_formula");
cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"], cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"],
file, sPath + ".cargo_capacity_formula"); file, sPath + ".cargo_capacity_formula");
cap.collectionRateFormula = requireFormula(sMt["collection_rate_formula"],
file, sPath + ".collection_rate_formula");
def.salvageCapability = std::move(cap); def.salvageCapability = std::move(cap);
} }
} }

View File

@@ -28,6 +28,7 @@ struct ModuleSalvageCapability
{ {
Formula collectionRangeFormula; Formula collectionRangeFormula;
Formula cargoCapacityFormula; Formula cargoCapacityFormula;
Formula collectionRateFormula;
}; };
struct ModuleRepairCapability struct ModuleRepairCapability

View File

@@ -5,4 +5,6 @@ struct SalvageCargoComponent
int capacity; int capacity;
int current; int current;
float collectionRange; float collectionRange;
int collectionIntervalTicks;
int cooldownTicksRemaining;
}; };

View File

@@ -379,6 +379,13 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo(); const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
// Tick down per-module collection cooldowns.
admin.forEach<SalvageCargoComponent>(
[](entt::entity /*e*/, SalvageCargoComponent& c)
{
if (c.cooldownTicksRemaining > 0) { --c.cooldownTicksRemaining; }
});
admin.forEach<SalvageBehaviorComponent, PositionComponent, admin.forEach<SalvageBehaviorComponent, PositionComponent,
SensorRangeComponent, MovementIntentComponent>( SensorRangeComponent, MovementIntentComponent>(
[&](entt::entity e, SalvageBehaviorComponent& salvageBehavior, [&](entt::entity e, SalvageBehaviorComponent& salvageBehavior,
@@ -461,27 +468,33 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
} }
if (retreating) { return; } if (retreating) { return; }
// Collect nearby scrap — increment first non-full salvage child. // Per-module independent collection: each ready module collects one scrap.
for (const ScrapInfo& si : allScrap) bool anythingCollected = false;
{
if ((si.position - pos.value).length() <= collectRange)
{
bool collected = false;
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>( admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
[&](entt::entity /*ce*/, SalvageCargoComponent& c, [&](entt::entity /*ce*/, SalvageCargoComponent& c,
const ModuleOwnerComponent& o) const ModuleOwnerComponent& o)
{ {
if (collected || o.owner != e || c.current >= c.capacity) { return; } if (o.owner != e || c.current >= c.capacity
|| c.cooldownTicksRemaining > 0)
{
return;
}
for (const ScrapInfo& si : allScrap)
{
if ((si.position - pos.value).length() > c.collectionRange) { continue; }
if (scraps.consume(si.entity)) if (scraps.consume(si.entity))
{ {
++c.current; ++c.current;
salvageBehavior.scrapTarget = std::nullopt; c.cooldownTicksRemaining = c.collectionIntervalTicks;
collected = true; anythingCollected = true;
}
});
break; break;
} }
} }
});
if (anythingCollected)
{
salvageBehavior.scrapTarget = std::nullopt;
}
// Move toward scrap target or find a new one. // Move toward scrap target or find a new one.
if (salvageBehavior.scrapTarget) if (salvageBehavior.scrapTarget)

View File

@@ -129,6 +129,11 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
cargo.current = 0; cargo.current = 0;
cargo.collectionRange = static_cast<float>( cargo.collectionRange = static_cast<float>(
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)); modDef->salvageCapability->collectionRangeFormula.evaluate(mx));
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
cargo.collectionIntervalTicks = (rate > 0.0)
? static_cast<int>(kTickRateHz / rate + 0.5)
: 0;
cargo.cooldownTicksRemaining = 0;
entt::entity child = m_admin.createModuleEntity(); entt::entity child = m_admin.createModuleEntity();
m_admin.addComponent<SalvageCargoComponent>(child, cargo); m_admin.addComponent<SalvageCargoComponent>(child, cargo);
@@ -245,10 +250,18 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child); SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child);
float fRange = c.collectionRange; float fRange = c.collectionRange;
float fCapacity = static_cast<float>(c.capacity); float fCapacity = static_cast<float>(c.capacity);
// Apply rate modifier: compute rate from interval, apply multiplier, convert back.
float fRate = (c.collectionIntervalTicks > 0)
? static_cast<float>(kTickRateHz) / static_cast<float>(c.collectionIntervalTicks)
: 0.0f;
applyMod(fRange, "collection_range", salvageMods); applyMod(fRange, "collection_range", salvageMods);
applyMod(fCapacity, "cargo_capacity", salvageMods); applyMod(fCapacity, "cargo_capacity", salvageMods);
applyMod(fRate, "collection_rate", salvageMods);
c.collectionRange = fRange; c.collectionRange = fRange;
c.capacity = static_cast<int>(fCapacity); c.capacity = static_cast<int>(fCapacity + 0.5f);
c.collectionIntervalTicks = (fRate > 0.0f)
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
: 0;
} }
// Apply repair modifiers to each repair child. // Apply repair modifiers to each repair child.