cargo component refactoring
This commit is contained in:
@@ -546,8 +546,8 @@ static const StatEntry kKnownStats[] = {
|
||||
{"weapon", "attack_range", "_m"},
|
||||
{"weapon", "attack_rate", "_hz"},
|
||||
{"salvage", "collection_range", "_m"},
|
||||
{"salvage", "cargo_capacity", ""},
|
||||
{"salvage", "collection_rate", "_hz"},
|
||||
{"cargo", "cargo_capacity", ""},
|
||||
{"repair", "repair_rate", "_hz"},
|
||||
{"repair", "repair_range", "_m"},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AttackBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BehaviorKind.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BehaviorScores.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CargoComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DeliverScrapBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DespawnAtComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodyComponent.h
|
||||
@@ -17,7 +18,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairToolComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RetreatBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvageCargoComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvageScrapBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapDataComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBehaviorComponent.h
|
||||
|
||||
10
src/lib/ecs/component/CargoComponent.h
Normal file
10
src/lib/ecs/component/CargoComponent.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// Single shared salvage cargo pool for a ship (REQ-MOD-CARGO-CAPACITY). Attached
|
||||
// to a ship at spawn only when its cargo capacity stat is greater than 0. All of
|
||||
// the ship's salvage modules deposit into this one pool; delivery draws from it.
|
||||
struct CargoComponent
|
||||
{
|
||||
int maxCapacity;
|
||||
int current;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
struct SalvageCargoComponent
|
||||
{
|
||||
int capacity;
|
||||
int current;
|
||||
float collectionRange_tiles;
|
||||
int collectionIntervalTicks;
|
||||
int cooldownTicksRemaining;
|
||||
};
|
||||
11
src/lib/ecs/component/SalvagerComponent.h
Normal file
11
src/lib/ecs/component/SalvagerComponent.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// Per-instance salvage collection emitter. Each placed salvage module owns one of
|
||||
// these and runs its own collection cycle on its own cooldown. The collected scrap
|
||||
// is stored in the owning ship's shared CargoComponent (REQ-SHP-SALVAGE), not here.
|
||||
struct SalvagerComponent
|
||||
{
|
||||
float collectionRange_tiles;
|
||||
int collectionIntervalTicks;
|
||||
int cooldownTicksRemaining;
|
||||
};
|
||||
@@ -6,13 +6,14 @@
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "CargoComponent.h"
|
||||
#include "DeliverScrapBehavior.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include <map>
|
||||
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "SalvagerComponent.h"
|
||||
#include "ScrapDataComponent.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "tracing.h"
|
||||
@@ -32,44 +33,53 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
|
||||
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
||||
|
||||
// Tick down per-module collection cooldowns.
|
||||
m_admin.forEach<SalvageCargoComponent>(
|
||||
[](entt::entity /*e*/, SalvageCargoComponent& c)
|
||||
m_admin.forEach<SalvagerComponent>(
|
||||
[](entt::entity /*e*/, SalvagerComponent& s)
|
||||
{
|
||||
if (c.cooldownTicksRemaining > 0) { --c.cooldownTicksRemaining; }
|
||||
if (s.cooldownTicksRemaining > 0) { --s.cooldownTicksRemaining; }
|
||||
});
|
||||
|
||||
// Scrap units already claimed by not-yet-applied collection cycles, so two
|
||||
// modules don't both target the last unit of the same pile (the claim would be
|
||||
// dropped at apply time). A pile is available while its amount exceeds its claims.
|
||||
std::map<entt::entity, int> claimedUnits;
|
||||
// Collection cycles already in flight toward each ship's shared cargo pool, so
|
||||
// concurrent modules on the same ship never start more cycles than the remaining
|
||||
// capacity can hold (REQ-SHP-SALVAGE).
|
||||
std::map<entt::entity, int> pendingByShip;
|
||||
for (const PendingCollection& pc : m_pendingCollections)
|
||||
{
|
||||
++claimedUnits[pc.scrap];
|
||||
++pendingByShip[pc.ship];
|
||||
}
|
||||
|
||||
// Cycle start: each ready, in-range module with free cargo begins a collection
|
||||
// cycle — emit the beam now, collect one scrap mid-beam, start the cooldown now.
|
||||
m_admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity moduleEntity, SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
||||
// Cycle start: each ready, in-range module whose ship's pool has free space begins
|
||||
// a collection cycle — emit the beam now, collect one scrap mid-beam, start the
|
||||
// cooldown now.
|
||||
m_admin.forEach<SalvagerComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*moduleEntity*/, SalvagerComponent& s, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (c.current >= c.capacity || c.cooldownTicksRemaining > 0) { return; }
|
||||
if (c.collectionIntervalTicks <= 0) { return; }
|
||||
if (!m_admin.hasAll<PositionComponent>(o.owner)) { return; }
|
||||
if (s.cooldownTicksRemaining > 0 || s.collectionIntervalTicks <= 0) { return; }
|
||||
if (!m_admin.hasAll<PositionComponent, CargoComponent>(o.owner)) { return; }
|
||||
|
||||
const CargoComponent& cargo = m_admin.get<CargoComponent>(o.owner);
|
||||
if (cargo.current + pendingByShip[o.owner] >= cargo.maxCapacity) { return; }
|
||||
|
||||
const QVector2D ownerPos = m_admin.get<PositionComponent>(o.owner).value;
|
||||
for (const ScrapInfo& si : allScrap)
|
||||
{
|
||||
if ((si.position - ownerPos).length() > c.collectionRange_tiles) { continue; }
|
||||
if ((si.position - ownerPos).length() > s.collectionRange_tiles) { continue; }
|
||||
if (claimedUnits[si.entity] >= m_admin.get<ScrapDataComponent>(si.entity).amount)
|
||||
{
|
||||
continue; // every remaining unit of this pile is already spoken for
|
||||
}
|
||||
outBeamFiredEvents.push_back(
|
||||
BeamFiredEvent{BeamKind::Salvage, o.owner, si.entity, currentTick});
|
||||
m_pendingCollections.push_back({moduleEntity, si.entity,
|
||||
m_pendingCollections.push_back({o.owner, si.entity,
|
||||
currentTick + kBeamImpactDelayTicks});
|
||||
++claimedUnits[si.entity];
|
||||
c.cooldownTicksRemaining = c.collectionIntervalTicks;
|
||||
++pendingByShip[o.owner];
|
||||
s.cooldownTicksRemaining = s.collectionIntervalTicks;
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -86,18 +96,14 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
|
||||
bay->anchor.y() + bay->footprint.height() / 2.0f);
|
||||
if ((pos.value - bayCenter).length() > 1.0f) { return; }
|
||||
|
||||
// Decrement the first non-empty salvage child belonging to this ship.
|
||||
bool delivered = false;
|
||||
m_admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*ce*/, SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (delivered || o.owner != ship || c.current <= 0) { return; }
|
||||
if (buildings.deliverScrapToSalvageBay(deliver.deliveryBay))
|
||||
{
|
||||
--c.current;
|
||||
delivered = true;
|
||||
}
|
||||
});
|
||||
// Hand over one unit from the ship's shared cargo pool.
|
||||
if (!m_admin.hasAll<CargoComponent>(ship)) { return; }
|
||||
CargoComponent& cargo = m_admin.get<CargoComponent>(ship);
|
||||
if (cargo.current <= 0) { return; }
|
||||
if (buildings.deliverScrapToSalvageBay(deliver.deliveryBay))
|
||||
{
|
||||
--cargo.current;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,12 +114,12 @@ void SalvagerSystem::applyPendingCollections(Tick currentTick, ScrapSystem& scra
|
||||
{
|
||||
if (it->appliesAt <= currentTick)
|
||||
{
|
||||
if (m_admin.isValid(it->module) && m_admin.hasAll<SalvageCargoComponent>(it->module))
|
||||
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
|
||||
{
|
||||
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(it->module);
|
||||
if (c.current < c.capacity && scraps.collectOne(it->scrap))
|
||||
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
|
||||
if (cargo.current < cargo.maxCapacity && scraps.collectOne(it->scrap))
|
||||
{
|
||||
++c.current;
|
||||
++cargo.current;
|
||||
}
|
||||
}
|
||||
it = m_pendingCollections.erase(it);
|
||||
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
private:
|
||||
struct PendingCollection
|
||||
{
|
||||
entt::entity module;
|
||||
entt::entity ship;
|
||||
entt::entity scrap;
|
||||
Tick appliesAt;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "AdvanceBehavior.h"
|
||||
#include "AttackBehavior.h"
|
||||
#include "BehaviorScores.h"
|
||||
#include "CargoComponent.h"
|
||||
#include "DeliverScrapBehavior.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
@@ -21,8 +22,8 @@
|
||||
#include "RepairBehavior.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "RetreatBehavior.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "SalvageScrapBehavior.h"
|
||||
#include "SalvagerComponent.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "StandbyBehavior.h"
|
||||
@@ -107,6 +108,10 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
std::vector<entt::entity> salvageChildren;
|
||||
std::vector<entt::entity> repairChildren;
|
||||
|
||||
// Cargo capacity is a ship-level stat (REQ-MOD-CARGO-CAPACITY): its base is the
|
||||
// sum of every cargo-providing module's contribution, accumulated here.
|
||||
double cargoCapacityBase = 0.0;
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
@@ -136,20 +141,19 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
|
||||
if (modDef->salvageCapability)
|
||||
{
|
||||
SalvageCargoComponent cargo;
|
||||
cargo.capacity = static_cast<int>(
|
||||
modDef->salvageCapability->cargoCapacityFormula.evaluate(mx));
|
||||
cargo.current = 0;
|
||||
cargo.collectionRange_tiles = static_cast<float>(
|
||||
cargoCapacityBase += modDef->salvageCapability->cargoCapacityFormula.evaluate(mx);
|
||||
|
||||
SalvagerComponent salvager;
|
||||
salvager.collectionRange_tiles = static_cast<float>(
|
||||
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize;
|
||||
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
|
||||
cargo.collectionIntervalTicks = (rate > 0.0)
|
||||
salvager.collectionIntervalTicks = (rate > 0.0)
|
||||
? static_cast<int>(kTickRateHz / rate + 0.5)
|
||||
: 0;
|
||||
cargo.cooldownTicksRemaining = 0;
|
||||
salvager.cooldownTicksRemaining = 0;
|
||||
|
||||
entt::entity child = m_admin.createModuleEntity();
|
||||
m_admin.addComponent<SalvageCargoComponent>(child, cargo);
|
||||
m_admin.addComponent<SalvagerComponent>(child, salvager);
|
||||
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
||||
salvageChildren.push_back(child);
|
||||
}
|
||||
@@ -184,6 +188,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
std::map<std::string, std::pair<double, double>> weaponMods;
|
||||
std::map<std::string, std::pair<double, double>> salvageMods;
|
||||
std::map<std::string, std::pair<double, double>> repairMods;
|
||||
// Ship-level cargo capacity modifiers ([module.cargo]); applied to the pool.
|
||||
std::map<std::string, std::pair<double, double>> cargoMods;
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
@@ -204,15 +210,16 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
const bool isWeaponStat = (sm.stat == "damage"
|
||||
|| sm.stat == "attack_range"
|
||||
|| sm.stat == "attack_rate");
|
||||
const bool isSalvageStat = (sm.stat == "collection_range"
|
||||
|| sm.stat == "cargo_capacity");
|
||||
const bool isSalvageStat = (sm.stat == "collection_range");
|
||||
const bool isRepairStat = (sm.stat == "repair_rate"
|
||||
|| sm.stat == "repair_range");
|
||||
const bool isCargoStat = (sm.stat == "cargo_capacity");
|
||||
|
||||
std::map<std::string, std::pair<double, double>>* target = &hullMods;
|
||||
if (isWeaponStat) { target = &weaponMods; }
|
||||
if (isSalvageStat) { target = &salvageMods; }
|
||||
if (isRepairStat) { target = &repairMods; }
|
||||
if (isCargoStat) { target = &cargoMods; }
|
||||
|
||||
std::pair<double, double>& acc = (*target)[sm.stat];
|
||||
if (sm.modifierType == "multiplicative")
|
||||
@@ -305,23 +312,33 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
// Apply salvage modifiers to each salvage child.
|
||||
for (entt::entity child : salvageChildren)
|
||||
{
|
||||
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child);
|
||||
float fRange = c.collectionRange_tiles;
|
||||
float fCapacity = static_cast<float>(c.capacity);
|
||||
SalvagerComponent& c = m_admin.get<SalvagerComponent>(child);
|
||||
float fRange = c.collectionRange_tiles;
|
||||
// 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(fCapacity, "cargo_capacity", salvageMods);
|
||||
applyMod(fRate, "collection_rate", salvageMods);
|
||||
applyMod(fRange, "collection_range", salvageMods);
|
||||
applyMod(fRate, "collection_rate", salvageMods);
|
||||
c.collectionRange_tiles = fRange;
|
||||
c.capacity = static_cast<int>(fCapacity + 0.5f);
|
||||
c.collectionIntervalTicks = (fRate > 0.0f)
|
||||
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Cargo capacity is a ship-level stat: apply [module.cargo] modifiers to the
|
||||
// summed base, then attach the shared cargo pool when the ship can hold anything
|
||||
// (REQ-MOD-CARGO-CAPACITY).
|
||||
{
|
||||
float fCapacity = static_cast<float>(cargoCapacityBase);
|
||||
applyMod(fCapacity, "cargo_capacity", cargoMods);
|
||||
const int maxCapacity = static_cast<int>(fCapacity + 0.5f);
|
||||
if (maxCapacity > 0)
|
||||
{
|
||||
m_admin.addComponent<CargoComponent>(entity, CargoComponent{maxCapacity, 0});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply repair modifiers to each repair child.
|
||||
for (entt::entity child : repairChildren)
|
||||
{
|
||||
@@ -383,7 +400,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
float maxCollRange = 0.0f;
|
||||
for (entt::entity child : salvageChildren)
|
||||
{
|
||||
const float r = m_admin.get<SalvageCargoComponent>(child).collectionRange_tiles;
|
||||
const float r = m_admin.get<SalvagerComponent>(child).collectionRange_tiles;
|
||||
if (r > maxCollRange) { maxCollRange = r; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#include "BehaviorTargeting.h"
|
||||
|
||||
#include "CargoComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "HqProxyComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "StationBodyComponent.h"
|
||||
|
||||
@@ -72,13 +71,10 @@ std::vector<CombatantInfo> buildCombatants(EntityAdmin& admin)
|
||||
std::unordered_map<entt::entity, CargoState> buildCargoByShip(EntityAdmin& admin)
|
||||
{
|
||||
std::unordered_map<entt::entity, CargoState> cargoByShip;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&cargoByShip](entt::entity /*ce*/, const SalvageCargoComponent& c,
|
||||
const ModuleOwnerComponent& o)
|
||||
admin.forEach<CargoComponent>(
|
||||
[&cargoByShip](entt::entity ship, const CargoComponent& c)
|
||||
{
|
||||
CargoState& agg = cargoByShip[o.owner];
|
||||
agg.current += c.current;
|
||||
agg.capacity += c.capacity;
|
||||
cargoByShip[ship] = CargoState{c.current, c.maxCapacity};
|
||||
});
|
||||
return cargoByShip;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ std::vector<RepairableInfo> buildRepairables(EntityAdmin& admin);
|
||||
// All ships, stations, and the HQ proxy — candidates for attack targeting.
|
||||
std::vector<CombatantInfo> buildCombatants(EntityAdmin& admin);
|
||||
|
||||
// Aggregated salvage cargo per owning ship, summed across its salvage modules.
|
||||
// Salvage cargo pool per ship, read from each ship's shared CargoComponent.
|
||||
std::unordered_map<entt::entity, CargoState> buildCargoByShip(EntityAdmin& admin);
|
||||
|
||||
// True when the ship's aggregated cargo is at capacity (and it has any capacity).
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "CargoComponent.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "SalvagerComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "Tick.h"
|
||||
#include "WeaponComponent.h"
|
||||
@@ -66,6 +67,10 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
std::vector<SalvageInstance> salvageInstances;
|
||||
std::vector<RepairInstance> repairInstances;
|
||||
|
||||
// Cargo capacity is a ship-level stat (REQ-MOD-CARGO-CAPACITY): base is the sum
|
||||
// of every salvage module's contribution.
|
||||
double cargoCapacityBase = 0.0;
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||
@@ -85,6 +90,8 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
}
|
||||
if (def->salvageCapability)
|
||||
{
|
||||
cargoCapacityBase += def->salvageCapability->cargoCapacityFormula.evaluate(mx);
|
||||
|
||||
SalvageInstance si;
|
||||
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize);
|
||||
si.rate = static_cast<float>(def->salvageCapability->collectionRateFormula.evaluate(mx));
|
||||
@@ -106,6 +113,7 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
std::map<std::string, std::pair<double, double>> weaponMods;
|
||||
std::map<std::string, std::pair<double, double>> salvageMods;
|
||||
std::map<std::string, std::pair<double, double>> repairMods;
|
||||
std::map<std::string, std::pair<double, double>> cargoMods;
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
@@ -123,15 +131,16 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
const bool isWeaponStat = (sm.stat == "damage"
|
||||
|| sm.stat == "attack_range"
|
||||
|| sm.stat == "attack_rate");
|
||||
const bool isSalvageStat = (sm.stat == "collection_range"
|
||||
|| sm.stat == "cargo_capacity");
|
||||
const bool isSalvageStat = (sm.stat == "collection_range");
|
||||
const bool isRepairStat = (sm.stat == "repair_rate"
|
||||
|| sm.stat == "repair_range");
|
||||
const bool isCargoStat = (sm.stat == "cargo_capacity");
|
||||
|
||||
std::map<std::string, std::pair<double, double>>* target = &hullMods;
|
||||
if (isWeaponStat) { target = &weaponMods; }
|
||||
if (isSalvageStat) { target = &salvageMods; }
|
||||
if (isRepairStat) { target = &repairMods; }
|
||||
if (isCargoStat) { target = &cargoMods; }
|
||||
|
||||
std::pair<double, double>& acc = (*target)[sm.stat];
|
||||
if (sm.modifierType == "multiplicative")
|
||||
@@ -232,6 +241,14 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
result.salvage = ShipStats::SalvageStats{combinedRate, maxRange};
|
||||
}
|
||||
|
||||
// Cargo capacity is a ship-level stat: apply [module.cargo] modifiers to the
|
||||
// summed base (REQ-MOD-CARGO-CAPACITY).
|
||||
{
|
||||
float capacity = static_cast<float>(cargoCapacityBase);
|
||||
applyMod(capacity, "cargo_capacity", cargoMods);
|
||||
result.cargoCapacity = static_cast<int>(capacity + 0.5f);
|
||||
}
|
||||
|
||||
// Apply repair modifiers and compute combined stats.
|
||||
if (!repairInstances.empty())
|
||||
{
|
||||
@@ -287,8 +304,8 @@ ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEn
|
||||
if (w.range_tiles > weaponMaxRange) { weaponMaxRange = w.range_tiles; }
|
||||
});
|
||||
|
||||
admin.forEach<ModuleOwnerComponent, SalvageCargoComponent>(
|
||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const SalvageCargoComponent& s)
|
||||
admin.forEach<ModuleOwnerComponent, SalvagerComponent>(
|
||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const SalvagerComponent& s)
|
||||
{
|
||||
if (owner.owner != shipEntity) { return; }
|
||||
hasSalvage = true;
|
||||
@@ -299,6 +316,12 @@ ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEn
|
||||
if (s.collectionRange_tiles > salvageMaxRange) { salvageMaxRange = s.collectionRange_tiles; }
|
||||
});
|
||||
|
||||
admin.forEach<CargoComponent>(
|
||||
[&](entt::entity ship, const CargoComponent& c)
|
||||
{
|
||||
if (ship == shipEntity) { result.cargoCapacity = c.maxCapacity; }
|
||||
});
|
||||
|
||||
admin.forEach<ModuleOwnerComponent, RepairToolComponent>(
|
||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const RepairToolComponent& r)
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ struct ShipStats
|
||||
float maneuveringAcceleration_tpss;
|
||||
float angularAcceleration_radpss;
|
||||
float maxRotationSpeed_radps;
|
||||
int cargoCapacity = 0;
|
||||
|
||||
struct WeaponStats
|
||||
{
|
||||
|
||||
@@ -33,9 +33,10 @@
|
||||
#include "RepairSystem.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "RetreatBehavior.h"
|
||||
#include "CargoComponent.h"
|
||||
#include "Rotation.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "SalvageScrapBehavior.h"
|
||||
#include "SalvagerComponent.h"
|
||||
#include "SalvagerSystem.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
@@ -179,8 +180,8 @@ static ShipLayoutConfig makeTwoModuleLayout(const std::string& moduleId)
|
||||
static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
entt::entity result = entt::null;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const SalvageCargoComponent&, const ModuleOwnerComponent& o)
|
||||
admin.forEach<SalvagerComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const SalvagerComponent&, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||
});
|
||||
@@ -944,7 +945,7 @@ TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]"
|
||||
|
||||
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||
REQUIRE(f.admin.isValid(sc));
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(sc).current == 1);
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == 1);
|
||||
REQUIRE_FALSE(f.admin.isValid(scrapEntity));
|
||||
}
|
||||
|
||||
@@ -969,10 +970,9 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
{
|
||||
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||
REQUIRE(f.admin.isValid(sc));
|
||||
SalvageCargoComponent& cargo = f.admin.get<SalvageCargoComponent>(sc);
|
||||
cargo.current = cargo.capacity; // full cargo
|
||||
REQUIRE(f.admin.isValid(firstSalvageChild(f.admin, ship)));
|
||||
CargoComponent& cargo = f.admin.get<CargoComponent>(ship);
|
||||
cargo.current = cargo.maxCapacity; // full cargo
|
||||
}
|
||||
|
||||
f.decide();
|
||||
@@ -990,13 +990,9 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
||||
|
||||
static int totalSalvageCurrent(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
int total = 0;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*ce*/, const SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship) { total += c.current; }
|
||||
});
|
||||
return total;
|
||||
return admin.hasAll<CargoComponent>(ship)
|
||||
? admin.get<CargoComponent>(ship).current
|
||||
: 0;
|
||||
}
|
||||
|
||||
TEST_CASE("SalvagerSystem: module does not collect scrap beyond its collection range",
|
||||
@@ -1011,7 +1007,7 @@ TEST_CASE("SalvagerSystem: module does not collect scrap beyond its collection r
|
||||
|
||||
f.runSalvageCollect();
|
||||
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).current == 0);
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("SalvagerSystem: module collects scrap within its collection range",
|
||||
@@ -1026,7 +1022,7 @@ TEST_CASE("SalvagerSystem: module collects scrap within its collection range",
|
||||
|
||||
f.runSalvageCollect();
|
||||
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).current == 1);
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1045,10 +1041,10 @@ TEST_CASE("SalvagerSystem: collection sets cooldown on module", "[behavior]")
|
||||
// collected until mid-beam (REQ-SHP-SALVAGE), so cargo is still empty now.
|
||||
f.salvageTick();
|
||||
|
||||
const SalvageCargoComponent& cargo =
|
||||
f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship));
|
||||
REQUIRE(cargo.current == 0);
|
||||
REQUIRE(cargo.cooldownTicksRemaining == cargo.collectionIntervalTicks);
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == 0);
|
||||
const SalvagerComponent& salvager =
|
||||
f.admin.get<SalvagerComponent>(firstSalvageChild(f.admin, ship));
|
||||
REQUIRE(salvager.cooldownTicksRemaining == salvager.collectionIntervalTicks);
|
||||
}
|
||||
|
||||
TEST_CASE("SalvagerSystem: module on cooldown does not collect scrap", "[behavior]")
|
||||
@@ -1059,11 +1055,11 @@ TEST_CASE("SalvagerSystem: module on cooldown does not collect scrap", "[behavio
|
||||
false, salvageLayout);
|
||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
|
||||
f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).cooldownTicksRemaining = 10;
|
||||
f.admin.get<SalvagerComponent>(firstSalvageChild(f.admin, ship)).cooldownTicksRemaining = 10;
|
||||
|
||||
f.runSalvageCollect();
|
||||
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).current == 0);
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("SalvagerSystem: module collects again after cooldown expires", "[behavior]")
|
||||
@@ -1076,17 +1072,17 @@ TEST_CASE("SalvagerSystem: module collects again after cooldown expires", "[beha
|
||||
|
||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
f.runSalvageCollect();
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(sc).current == 1);
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == 1);
|
||||
|
||||
// Shorten cooldown to 1 tick and place a second scrap.
|
||||
f.admin.get<SalvageCargoComponent>(sc).cooldownTicksRemaining = 1;
|
||||
f.admin.get<SalvagerComponent>(sc).cooldownTicksRemaining = 1;
|
||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
|
||||
// Once the cooldown expires the module starts another cycle and collects the
|
||||
// second scrap after the mid-beam delay.
|
||||
f.runSalvageCollect();
|
||||
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(sc).current == 2);
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == 2);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1119,12 +1115,12 @@ TEST_CASE("SalvagerSystem: second salvage module does not collect when first is
|
||||
|
||||
// Put the first salvage child on cooldown.
|
||||
entt::entity blocked = entt::null;
|
||||
f.admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
||||
f.admin.forEach<SalvagerComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, SalvagerComponent& s, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && blocked == entt::null)
|
||||
{
|
||||
c.cooldownTicksRemaining = 99;
|
||||
s.cooldownTicksRemaining = 99;
|
||||
blocked = ce;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
#include "RepairBehavior.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "RetreatBehavior.h"
|
||||
#include "CargoComponent.h"
|
||||
#include "Rotation.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "SalvageScrapBehavior.h"
|
||||
#include "SalvagerComponent.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "ShipLayout.h"
|
||||
@@ -52,8 +53,8 @@ static entt::entity firstWeaponChild(EntityAdmin& admin, entt::entity ship)
|
||||
static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
entt::entity result = entt::null;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const SalvageCargoComponent&, const ModuleOwnerComponent& o)
|
||||
admin.forEach<SalvagerComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const SalvagerComponent&, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||
});
|
||||
@@ -220,8 +221,9 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
||||
// salvager: cargo_capacity_formula = "10", collection_range_m_formula = "500" m → 500/10 = 50 tiles
|
||||
const entt::entity sc = firstSalvageChild(admin, e);
|
||||
REQUIRE(admin.isValid(sc));
|
||||
REQUIRE(admin.get<SalvageCargoComponent>(sc).capacity == 10);
|
||||
REQUIRE(admin.get<SalvageCargoComponent>(sc).current == 0);
|
||||
// Cargo capacity is now a ship-level pool (REQ-MOD-CARGO-CAPACITY).
|
||||
REQUIRE(admin.get<CargoComponent>(e).maxCapacity == 10);
|
||||
REQUIRE(admin.get<CargoComponent>(e).current == 0);
|
||||
REQUIRE(admin.get<DeliverScrapBehavior>(e).deliveryBay == kInvalidBuildingId);
|
||||
REQUIRE_FALSE(admin.get<SalvageScrapBehavior>(e).scrapTarget.has_value());
|
||||
REQUIRE(admin.get<SalvageScrapBehavior>(e).maxCollectionRange_tiles == Approx(50.0f));
|
||||
|
||||
@@ -50,6 +50,8 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
|
||||
m_maneuveringAccelLabel = makeStatLabel(this);
|
||||
m_angularAccelLabel = makeStatLabel(this);
|
||||
m_maxRotSpeedLabel = makeStatLabel(this);
|
||||
m_cargoCapacityLabel = makeStatLabel(this);
|
||||
m_cargoCapacityLabel->setVisible(false);
|
||||
|
||||
layout->addWidget(m_hpLabel);
|
||||
layout->addWidget(m_speedLabel);
|
||||
@@ -58,6 +60,7 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
|
||||
layout->addWidget(m_maneuveringAccelLabel);
|
||||
layout->addWidget(m_angularAccelLabel);
|
||||
layout->addWidget(m_maxRotSpeedLabel);
|
||||
layout->addWidget(m_cargoCapacityLabel);
|
||||
|
||||
// Weapon capability section.
|
||||
m_weaponSection = new QWidget(this);
|
||||
@@ -151,6 +154,19 @@ void ShipStatsPanel::applyStats(const ShipStats& stats, const QString& hpText)
|
||||
m_maxRotSpeedLabel->setText(
|
||||
tr("Max Rotation: %1 rad/s").arg(fmt(stats.maxRotationSpeed_radps)));
|
||||
|
||||
// Cargo capacity is shown only when the ship can actually hold cargo
|
||||
// (REQ-MOD-UI-STATS-PANEL).
|
||||
if (stats.cargoCapacity > 0)
|
||||
{
|
||||
m_cargoCapacityLabel->setText(
|
||||
tr("Cargo Capacity: %1").arg(stats.cargoCapacity));
|
||||
m_cargoCapacityLabel->setVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_cargoCapacityLabel->setVisible(false);
|
||||
}
|
||||
|
||||
if (stats.weapons.has_value())
|
||||
{
|
||||
m_weaponDpsLabel->setText(
|
||||
|
||||
@@ -42,6 +42,7 @@ private:
|
||||
QLabel* m_maneuveringAccelLabel;
|
||||
QLabel* m_angularAccelLabel;
|
||||
QLabel* m_maxRotSpeedLabel;
|
||||
QLabel* m_cargoCapacityLabel;
|
||||
|
||||
QWidget* m_weaponSection;
|
||||
QLabel* m_weaponDpsLabel;
|
||||
|
||||
Reference in New Issue
Block a user