define ship roles via added modules and allow multiple weapons

This commit is contained in:
2026-06-01 22:57:53 +02:00
parent f363f7a67c
commit 9d0a60a93b
31 changed files with 873 additions and 407 deletions

View File

@@ -3,11 +3,13 @@
#include <cassert>
#include <map>
#include <utility>
#include <vector>
#include "DynamicBodyComponent.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "ModuleOwnerComponent.h"
#include "ModulesConfig.h"
#include "MovementIntentComponent.h"
#include "RallyBehaviorComponent.h"
@@ -85,17 +87,182 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
angularAccelPerTick, maxRotationSpeedPerTick, sensorRange,
level, schematicId, isEnemy);
// Optional components based on ship role.
if (def->combat)
{
WeaponComponent w;
w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x));
w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x));
w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x));
w.cooldownTicks = 0.0f;
w.currentTarget = std::nullopt;
m_admin.addComponent<WeaponComponent>(entity, w);
// Determine module list: configured layout takes precedence over default.
const std::vector<PlacedModule>& modules =
layout.has_value() ? layout->placedModules : def->defaultModules;
// --- Pass 1: create capability child entities ----------------------------
std::vector<entt::entity> weaponChildren;
std::vector<entt::entity> salvageChildren;
std::vector<entt::entity> repairChildren;
for (const PlacedModule& pm : modules)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef) { continue; }
const double mx = static_cast<double>(modDef->playerProductionLevel);
if (modDef->weaponCapability)
{
WeaponComponent w;
w.damage = static_cast<float>(
modDef->weaponCapability->damageFormula.evaluate(mx));
w.range = static_cast<float>(
modDef->weaponCapability->attackRangeFormula.evaluate(mx));
w.fireRateHz = static_cast<float>(
modDef->weaponCapability->attackRateFormula.evaluate(mx));
w.cooldownTicks = 0.0f;
w.currentTarget = std::nullopt;
entt::entity child = m_admin.createModuleEntity();
m_admin.addComponent<WeaponComponent>(child, w);
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
weaponChildren.push_back(child);
}
if (modDef->salvageCapability)
{
SalvageCargoComponent cargo;
cargo.capacity = static_cast<int>(
modDef->salvageCapability->cargoCapacityFormula.evaluate(mx));
cargo.current = 0;
cargo.collectionRange = static_cast<float>(
modDef->salvageCapability->collectionRangeFormula.evaluate(mx));
entt::entity child = m_admin.createModuleEntity();
m_admin.addComponent<SalvageCargoComponent>(child, cargo);
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
salvageChildren.push_back(child);
}
if (modDef->repairCapability)
{
RepairToolComponent rt;
rt.ratePerTick = static_cast<float>(
modDef->repairCapability->repairRateFormula.evaluate(mx))
/ static_cast<float>(kTickRateHz);
rt.range = static_cast<float>(
modDef->repairCapability->repairRangeFormula.evaluate(mx));
rt.currentTarget = std::nullopt;
entt::entity child = m_admin.createModuleEntity();
m_admin.addComponent<RepairToolComponent>(child, rt);
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
repairChildren.push_back(child);
}
}
// --- Pass 2: apply passive stat modifiers --------------------------------
// Accumulate hull-level modifiers.
std::map<std::string, std::pair<double, double>> hullMods;
// Per-capability-type modifier accumulators (applied to each child).
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;
for (const PlacedModule& pm : modules)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef) { continue; }
const double mx = static_cast<double>(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<std::string, std::pair<double, double>>* target = &hullMods;
if (isWeaponStat) { target = &weaponMods; }
if (isSalvageStat) { target = &salvageMods; }
if (isRepairStat) { target = &repairMods; }
std::pair<double, double>& acc = (*target)[sm.stat];
if (sm.modifierType == "multiplicative")
{
acc.first += (val - 1.0);
}
else
{
acc.second += val;
}
}
}
// Helper: apply a modifier map to a float stat.
auto applyMod = [](float& stat, const std::string& name,
const std::map<std::string, std::pair<double, double>>& mods)
{
const auto it = mods.find(name);
if (it != mods.end())
{
stat = static_cast<float>(
static_cast<double>(stat) * (1.0 + it->second.first)
+ it->second.second);
}
};
// Apply hull modifiers.
{
HealthComponent& health = m_admin.get<HealthComponent>(entity);
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
SensorRangeComponent& sensor = m_admin.get<SensorRangeComponent>(entity);
applyMod(health.maxHp, "hp", hullMods);
health.hp = health.maxHp;
applyMod(dynamics.maxSpeedPerTick, "speed", hullMods);
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration", hullMods);
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration", hullMods);
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration", hullMods);
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed", hullMods);
applyMod(sensor.value, "sensor_range", hullMods);
}
// Apply weapon modifiers to each weapon child.
for (entt::entity child : weaponChildren)
{
WeaponComponent& w = m_admin.get<WeaponComponent>(child);
applyMod(w.damage, "damage", weaponMods);
applyMod(w.range, "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<SalvageCargoComponent>(child);
float fRange = c.collectionRange;
float fCapacity = static_cast<float>(c.capacity);
applyMod(fRange, "collection_range", salvageMods);
applyMod(fCapacity, "cargo_capacity", salvageMods);
c.collectionRange = fRange;
c.capacity = static_cast<int>(fCapacity);
}
// Apply repair modifiers to each repair child.
for (entt::entity child : repairChildren)
{
RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child);
applyMod(rt.ratePerTick, "repair_rate", repairMods);
applyMod(rt.range, "repair_range", repairMods);
}
// --- Pass 3: attach behavior components based on capability presence -----
if (!weaponChildren.empty())
{
m_admin.addComponent<ThreatResponseBehaviorComponent>(
entity, ThreatResponseBehaviorComponent{});
@@ -106,95 +273,35 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
}
}
if (def->salvage)
if (!salvageChildren.empty())
{
SalvageCargoComponent cargo;
cargo.capacity = def->salvage->cargoCapacity;
cargo.current = 0;
cargo.collectionRange = static_cast<float>(def->salvage->collectionRange);
m_admin.addComponent<SalvageCargoComponent>(entity, cargo);
float maxCollRange = 0.0f;
for (entt::entity child : salvageChildren)
{
const float r = m_admin.get<SalvageCargoComponent>(child).collectionRange;
if (r > maxCollRange) { maxCollRange = r; }
}
SalvageBehaviorComponent salvageBehavior;
salvageBehavior.scrapTarget = std::nullopt;
salvageBehavior.deliveryBay = kInvalidBuildingId;
m_admin.addComponent<SalvageBehaviorComponent>(entity, salvageBehavior);
SalvageBehaviorComponent sb;
sb.scrapTarget = std::nullopt;
sb.deliveryBay = kInvalidBuildingId;
sb.maxCollectionRange = maxCollRange;
m_admin.addComponent<SalvageBehaviorComponent>(entity, sb);
}
if (def->repair)
if (!repairChildren.empty())
{
RepairToolComponent rt;
rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x));
rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x));
rt.currentTarget = std::nullopt;
m_admin.addComponent<RepairToolComponent>(entity, rt);
m_admin.addComponent<RepairBehaviorComponent>(entity, RepairBehaviorComponent{});
}
// Apply module stat modifiers (REQ-MOD-STAT-CALC).
if (layout.has_value() && !layout->placedModules.empty())
{
std::map<std::string, std::pair<double, double>> mods;
for (const PlacedModule& pm : layout->placedModules)
float maxRepairRange = 0.0f;
for (entt::entity child : repairChildren)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef)
{
continue;
}
for (const ModuleStatModifier& sm : modDef->statModifiers)
{
const double val = sm.formula.evaluate(
static_cast<double>(modDef->playerProductionLevel));
std::pair<double, double>& acc = mods[sm.stat];
if (sm.modifierType == "multiplicative")
{
acc.first += (val - 1.0);
}
else
{
acc.second += val;
}
}
const float r = m_admin.get<RepairToolComponent>(child).range;
if (r > maxRepairRange) { maxRepairRange = r; }
}
auto applyMod = [&mods](float& stat, const std::string& name) {
const std::map<std::string, std::pair<double, double>>::const_iterator it =
mods.find(name);
if (it != mods.end())
{
stat = static_cast<float>(
static_cast<double>(stat) * (1.0 + it->second.first)
+ it->second.second);
}
};
HealthComponent& health = m_admin.get<HealthComponent>(entity);
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
SensorRangeComponent& sensor = m_admin.get<SensorRangeComponent>(entity);
applyMod(health.maxHp, "hp");
health.hp = health.maxHp;
applyMod(dynamics.maxSpeedPerTick, "speed");
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration");
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration");
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration");
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed");
applyMod(sensor.value, "sensor_range");
if (m_admin.hasAll<WeaponComponent>(entity))
{
WeaponComponent& weapon = m_admin.get<WeaponComponent>(entity);
applyMod(weapon.damage, "damage");
applyMod(weapon.range, "attack_range");
applyMod(weapon.fireRateHz, "attack_rate");
}
if (m_admin.hasAll<RepairToolComponent>(entity))
{
RepairToolComponent& repairTool = m_admin.get<RepairToolComponent>(entity);
applyMod(repairTool.ratePerTick, "repair_rate");
applyMod(repairTool.range, "repair_range");
}
RepairBehaviorComponent rb;
rb.currentTarget = std::nullopt;
rb.maxRepairRange = maxRepairRange;
m_admin.addComponent<RepairBehaviorComponent>(entity, rb);
}
return entity;
@@ -202,6 +309,13 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
void ShipSystem::despawn(entt::entity entity)
{
std::vector<entt::entity> children;
m_admin.forEach<ModuleOwnerComponent>(
[&](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);
}