#include "ShipSystem.h" #include #include #include #include #include #include "AdvanceBehavior.h" #include "AttackBehavior.h" #include "BehaviorScores.h" #include "DeliverScrapBehavior.h" #include "DynamicBodyComponent.h" #include "EntityAdmin.h" #include "FactionComponent.h" #include "HealthComponent.h" #include "ModuleOwnerComponent.h" #include "ModulesConfig.h" #include "MovementIntentComponent.h" #include "RallyBehavior.h" #include "RepairBehavior.h" #include "RepairToolComponent.h" #include "RetreatBehavior.h" #include "SalvageCargoComponent.h" #include "SalvageScrapBehavior.h" #include "SelectedBehaviorComponent.h" #include "SensorRangeComponent.h" #include "StandbyBehavior.h" #include "Tick.h" #include "tracing.h" #include "WeaponComponent.h" ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin) : m_config(config) , m_admin(admin) { } const ShipDef* ShipSystem::findShipDef(const std::string& schematicId) const { for (const ShipDef& def : m_config.ships.ships) { if (def.id == schematicId) { return &def; } } return nullptr; } const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const { for (const ModuleDef& def : m_config.modules.modules) { if (def.id == id) { return &def; } } return nullptr; } entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVector2D position, bool isEnemy, const std::optional& layout, const std::map& moduleLevelOverrides) { const ShipDef* def = findShipDef(schematicId); assert(def != nullptr); const double x = static_cast(level); const float tickRate = static_cast(kTickRateHz); const float tileSize = static_cast(m_config.world.tileSize_m); float hp = static_cast(def->health.hpFormula.evaluate(x)); float maxHp = hp; float maxSpeed_tpt = static_cast(def->movement.speedFormula.evaluate(x)) / tileSize / tickRate; float mainAcceleration_tptt = static_cast( def->movement.mainAccelerationFormula.evaluate(x)) / tileSize / tickRate; float maneuveringAcceleration_tptt = static_cast( def->movement.maneuveringAccelerationFormula.evaluate(x)) / tileSize / tickRate; float maxAngularAcceleration_rptt = static_cast( def->movement.angularAccelerationFormula.evaluate(x)) / tickRate; float maxRotationSpeed_rpt = static_cast( def->movement.maxRotationSpeedFormula.evaluate(x)) / tickRate; float sensorRange_tiles = static_cast( def->sensor.sensorRangeFormula.evaluate(x)) / tileSize; entt::entity entity = m_admin.spawnShip( position, hp, maxHp, maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt, maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles, level, schematicId, isEnemy); // Determine module list: configured layout takes precedence over default. const std::vector& modules = layout.has_value() ? layout->placedModules : def->defaultModules; // --- Pass 1: create capability child entities ---------------------------- std::vector weaponChildren; std::vector salvageChildren; std::vector repairChildren; for (const PlacedModule& pm : modules) { const ModuleDef* modDef = findModuleDef(pm.moduleId); if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); } const auto overIt = moduleLevelOverrides.find(pm.moduleId); const double mx = static_cast( overIt != moduleLevelOverrides.end() ? overIt->second : modDef->playerProductionLevel); if (modDef->weaponCapability) { WeaponComponent w; w.damage = static_cast( modDef->weaponCapability->damageFormula.evaluate(mx)); w.range_tiles = static_cast( modDef->weaponCapability->attackRangeFormula.evaluate(mx)) / tileSize; w.fireRateHz = static_cast( modDef->weaponCapability->attackRateFormula.evaluate(mx)); w.cooldownTicks = 0.0f; w.currentTarget = std::nullopt; entt::entity child = m_admin.createModuleEntity(); m_admin.addComponent(child, w); m_admin.addComponent(child, ModuleOwnerComponent{entity}); weaponChildren.push_back(child); } if (modDef->salvageCapability) { SalvageCargoComponent cargo; cargo.capacity = static_cast( modDef->salvageCapability->cargoCapacityFormula.evaluate(mx)); cargo.current = 0; cargo.collectionRange_tiles = static_cast( modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize; const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx); cargo.collectionIntervalTicks = (rate > 0.0) ? static_cast(kTickRateHz / rate + 0.5) : 0; cargo.cooldownTicksRemaining = 0; entt::entity child = m_admin.createModuleEntity(); m_admin.addComponent(child, cargo); m_admin.addComponent(child, ModuleOwnerComponent{entity}); salvageChildren.push_back(child); } if (modDef->repairCapability) { RepairToolComponent rt; rt.ratePerTick = static_cast( modDef->repairCapability->repairRateFormula.evaluate(mx)) / static_cast(kTickRateHz); rt.range_tiles = static_cast( modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize; rt.currentTarget = std::nullopt; entt::entity child = m_admin.createModuleEntity(); m_admin.addComponent(child, rt); m_admin.addComponent(child, ModuleOwnerComponent{entity}); repairChildren.push_back(child); } } // --- Pass 2: apply passive stat modifiers -------------------------------- // Accumulate hull-level modifiers. std::map> hullMods; // Per-capability-type modifier accumulators (applied to each child). std::map> weaponMods; std::map> salvageMods; std::map> repairMods; for (const PlacedModule& pm : modules) { const ModuleDef* modDef = findModuleDef(pm.moduleId); if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); } const auto overIt2 = moduleLevelOverrides.find(pm.moduleId); const double mx = static_cast( overIt2 != moduleLevelOverrides.end() ? overIt2->second : modDef->playerProductionLevel); for (const ModuleStatModifier& sm : modDef->statModifiers) { const double val = sm.formula.evaluate(mx); // Route modifier to the correct accumulator by stat category. // weapon/salvage/repair stats go to the corresponding child map; // hull stats (hp, speed, sensor_range, …) go to hullMods. 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 isRepairStat = (sm.stat == "repair_rate" || sm.stat == "repair_range"); std::map>* target = &hullMods; if (isWeaponStat) { target = &weaponMods; } if (isSalvageStat) { target = &salvageMods; } if (isRepairStat) { target = &repairMods; } std::pair& acc = (*target)[sm.stat]; if (sm.modifierType == "multiplicative") { acc.first += (val - 1.0); } else { acc.second += val; } } } // Range stat additive modifiers are expressed in metres in config; convert to tiles. const double tileSizeD = static_cast(m_config.world.tileSize_m); const double tickRateD = static_cast(kTickRateHz); const char* const kRangeStats[] = { "sensor_range", "attack_range", "collection_range", "repair_range" }; std::map>* allModMaps[] = { &hullMods, &weaponMods, &salvageMods, &repairMods }; for (const char* stat : kRangeStats) { for (std::map>* mods : allModMaps) { std::map>::iterator it = mods->find(stat); if (it != mods->end()) { it->second.second /= tileSizeD; } } } // Acceleration additive modifiers are in m/s² in config; convert to tiles/tick // (same as the base spawn conversion: / tileSize / tickRate). const char* const kAccelerationStats[] = { "main_acceleration", "maneuvering_acceleration" }; for (const char* stat : kAccelerationStats) { for (std::map>* mods : allModMaps) { std::map>::iterator it = mods->find(stat); if (it != mods->end()) { it->second.second /= tileSizeD * tickRateD; } } } // Helper: apply a modifier map to a float stat. auto applyMod = [](float& stat, const std::string& name, const std::map>& mods) { const auto it = mods.find(name); if (it != mods.end()) { stat = static_cast( static_cast(stat) * (1.0 + it->second.first) + it->second.second); } }; // Apply hull modifiers. { HealthComponent& health = m_admin.get(entity); DynamicBodyComponent& dynamics = m_admin.get(entity); SensorRangeComponent& sensor = m_admin.get(entity); applyMod(health.maxHp, "hp", hullMods); health.hp = health.maxHp; applyMod(dynamics.maxSpeed_tpt, "speed", hullMods); applyMod(dynamics.mainAcceleration_tptt, "main_acceleration", hullMods); applyMod(dynamics.maneuveringAcceleration_tptt, "maneuvering_acceleration", hullMods); applyMod(dynamics.maxAngularAcceleration_rptt, "angular_acceleration", hullMods); applyMod(dynamics.maxRotationSpeed_rpt, "max_rotation_speed", hullMods); applyMod(sensor.value_tiles, "sensor_range", hullMods); } // Apply weapon modifiers to each weapon child. for (entt::entity child : weaponChildren) { WeaponComponent& w = m_admin.get(child); applyMod(w.damage, "damage", weaponMods); applyMod(w.range_tiles, "attack_range", weaponMods); applyMod(w.fireRateHz, "attack_rate", weaponMods); } // Apply salvage modifiers to each salvage child. for (entt::entity child : salvageChildren) { SalvageCargoComponent& c = m_admin.get(child); float fRange = c.collectionRange_tiles; float fCapacity = static_cast(c.capacity); // Apply rate modifier: compute rate from interval, apply multiplier, convert back. float fRate = (c.collectionIntervalTicks > 0) ? static_cast(kTickRateHz) / static_cast(c.collectionIntervalTicks) : 0.0f; applyMod(fRange, "collection_range", salvageMods); applyMod(fCapacity, "cargo_capacity", salvageMods); applyMod(fRate, "collection_rate", salvageMods); c.collectionRange_tiles = fRange; c.capacity = static_cast(fCapacity + 0.5f); c.collectionIntervalTicks = (fRate > 0.0f) ? static_cast(static_cast(kTickRateHz) / fRate + 0.5f) : 0; } // Apply repair modifiers to each repair child. for (entt::entity child : repairChildren) { RepairToolComponent& rt = m_admin.get(child); applyMod(rt.ratePerTick, "repair_rate", repairMods); applyMod(rt.range_tiles, "repair_range", repairMods); } // --- Pass 3: attach behavior components based on capability presence ----- // Baseline: every ship can always fall back to advancing, and needs a slot // for the per-tick behavior selection result. m_admin.addComponent(entity, AdvanceBehavior{}); m_admin.addComponent(entity, SelectedBehaviorComponent{}); // Player ships retreat to the rally point when threatened or badly damaged // (disabled by the balancing tool to keep arena fights symmetric). if (!isEnemy && m_retreatEnabled) { RetreatBehavior retreat; retreat.retreatHpFraction = BehaviorScores::kLowHpFraction; retreat.retreatPoint = m_rallyPoint; m_admin.addComponent(entity, retreat); } if (!weaponChildren.empty()) { float maxWeaponRange = 0.0f; for (entt::entity child : weaponChildren) { const float r = m_admin.get(child).range_tiles; if (r > maxWeaponRange) { maxWeaponRange = r; } } AttackBehavior attack; attack.orbitRadius_tiles = maxWeaponRange * static_cast(m_config.world.orbitFactor); m_admin.addComponent(entity, attack); if (!isEnemy) { RallyBehavior rally; rally.rallyPoint = m_rallyPoint; rally.orbitRadius_tiles = static_cast(m_config.world.rallyOrbitRadius_tiles); m_admin.addComponent(entity, rally); } } if (!salvageChildren.empty()) { float maxCollRange = 0.0f; for (entt::entity child : salvageChildren) { const float r = m_admin.get(child).collectionRange_tiles; if (r > maxCollRange) { maxCollRange = r; } } SalvageScrapBehavior salvage; salvage.scrapTarget = std::nullopt; salvage.maxCollectionRange_tiles = maxCollRange; salvage.orbitRadius_tiles = maxCollRange * static_cast(m_config.world.orbitFactor); m_admin.addComponent(entity, salvage); DeliverScrapBehavior deliver; deliver.deliveryBay = kInvalidBuildingId; m_admin.addComponent(entity, deliver); } if (!repairChildren.empty()) { float maxRepairRange = 0.0f; for (entt::entity child : repairChildren) { const float r = m_admin.get(child).range_tiles; if (r > maxRepairRange) { maxRepairRange = r; } } RepairBehavior repair; repair.currentTarget = std::nullopt; repair.maxRepairRange_tiles = maxRepairRange; repair.orbitRadius_tiles = maxRepairRange * static_cast(m_config.world.orbitFactor); m_admin.addComponent(entity, repair); // Repair-capable ships hold with the fleet (REQ-SHP-STANDBY) instead of // charging the enemy when no more urgent behavior applies; this applies // whether or not the ship also carries weapons. m_admin.addComponent(entity, StandbyBehavior{}); } return entity; } void ShipSystem::despawn(entt::entity entity) { std::vector children; m_admin.forEach( [&](entt::entity e, const ModuleOwnerComponent& o) { if (o.owner == entity) { children.push_back(e); } }); for (entt::entity child : children) { m_admin.destroy(child); } m_admin.destroy(entity); } void ShipSystem::clearMovementIntents() { TRACE(); m_admin.forEach( [](entt::entity /*e*/, MovementIntentComponent& i) { i = MovementIntentComponent{false, QVector2D(0.0f, 0.0f)}; }); } void ShipSystem::setRallyPoint(QVector2D point) { m_rallyPoint = point; } void ShipSystem::setRetreatEnabled(bool enabled) { m_retreatEnabled = enabled; } void ShipSystem::triggerRallyDeparture() { TRACE(); std::vector toRemove; m_admin.forEach( [&toRemove](entt::entity e, const RallyBehavior& /*rb*/, const FactionComponent& f) { if (!f.isEnemy) { toRemove.push_back(e); } }); for (entt::entity e : toRemove) { m_admin.removeComponent(e); } }