unify Weapon and StationWeapon components

This commit is contained in:
2026-05-22 22:06:30 +02:00
parent bd488db8ef
commit ea79d76953
7 changed files with 89 additions and 157 deletions

View File

@@ -114,7 +114,7 @@ void ArenaSimulation::placeStructures()
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy) auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
{ {
float hp = 0.0f; float hp = 0.0f;
StationWeapon weapon; Weapon weapon;
weapon.cooldownTicks = 0.0f; weapon.cooldownTicks = 0.0f;
weapon.currentTarget = std::nullopt; weapon.currentTarget = std::nullopt;
const double lv = static_cast<double>(entry.level); const double lv = static_cast<double>(entry.level);
@@ -155,7 +155,7 @@ void ArenaSimulation::placeStructures()
} }
const entt::entity stationEntity = m_admin.spawnStation( const entt::entity stationEntity = m_admin.spawnStation(
anchor, parsed.footprint, absCells, hp, hp, isEnemy); anchor, parsed.footprint, absCells, hp, hp, isEnemy);
m_admin.addComponent<StationWeapon>(stationEntity, weapon); m_admin.addComponent<Weapon>(stationEntity, weapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId()); m_buildingSystem->registerTileOccupancy(absCells, allocateId());
}; };
@@ -500,3 +500,4 @@ void ArenaSimulation::updateStatus()
std::lock_guard<std::mutex> lock(m_statusMutex); std::lock_guard<std::mutex> lock(m_statusMutex);
m_status = newStatus; m_status = newStatus;
} }

View File

@@ -84,8 +84,6 @@ struct StationBody
std::vector<QPoint> bodyCells; std::vector<QPoint> bodyCells;
}; };
// StationWeapon remains defined in Building.h.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Scrap components // Scrap components
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -105,3 +103,4 @@ struct DespawnAt
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
struct HqProxy { char unused = 0; }; struct HqProxy { char unused = 0; };

View File

@@ -56,17 +56,6 @@ struct ConstructionSite
std::optional<ShipLayoutConfig> shipLayout; std::optional<ShipLayoutConfig> shipLayout;
}; };
// Weapon state for stationary structures (defence stations).
// Distinct from Ship::Weapon; stations have no movement intent.
struct StationWeapon
{
float damage;
float range;
float fireRateHz;
float cooldownTicks;
std::optional<entt::entity> currentTarget;
};
// A fully constructed, operational building. // A fully constructed, operational building.
struct Building struct Building
{ {
@@ -90,3 +79,4 @@ struct Building
// Module layout for shipyards (REQ-MOD-LAYOUT). // Module layout for shipyards (REQ-MOD-LAYOUT).
std::optional<ShipLayoutConfig> shipLayout; std::optional<ShipLayoutConfig> shipLayout;
}; };

View File

@@ -16,147 +16,92 @@ void CombatSystem::tick(Tick currentTick,
std::vector<FireEvent>& outFireEvents) std::vector<FireEvent>& outFireEvents)
{ {
// Ship weapons. // Ship weapons.
admin.forEach<Weapon, ThreatResponse, Position>( admin.forEach<Weapon, ThreatResponse, Position, Faction>(
[&](entt::entity e, Weapon& weapon, ThreatResponse& threat, Position& pos) [&](entt::entity e, Weapon& weapon, ThreatResponse& threat, Position& pos, Faction& faction)
{ {
resolveShipWeapon(e, weapon, threat, pos, currentTick, admin, outFireEvents); weapon.currentTarget = threat.currentTarget;
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
}); });
// Station weapons. // Station weapons.
admin.forEach<StationWeapon, Position, Faction>( admin.forEach<Weapon, Position, Faction>(
[&](entt::entity e, StationWeapon& weapon, Position& pos, Faction& faction) [&](entt::entity e, Weapon& weapon, Position& pos, Faction& faction)
{ {
resolveStationWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents); resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
}); });
} }
void CombatSystem::resolveShipWeapon(entt::entity shipEntity, Weapon& weapon, void CombatSystem::resolveWeapon(
const ThreatResponse& threat, entt::entity shipEntity,
const Position& pos, Tick currentTick, Weapon& weapon,
EntityAdmin& admin, const Position& ownPos,
std::vector<FireEvent>& out) const Faction& ownFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out)
{ {
if (!threat.currentTarget) if (weapon.cooldownTicks > 0.0f)
{ {
return; weapon.cooldownTicks -= 1.0f;
} }
if (weapon.cooldownTicks > 0.0f)
{
return;
}
if (weapon.cooldownTicks > 0.0f) // Validate or clear existing target.
{ if (weapon.currentTarget)
weapon.cooldownTicks -= 1.0f; {
} const entt::entity t = *weapon.currentTarget;
if (weapon.cooldownTicks > 0.0f) if (!admin.isValid(t) || !admin.hasAll<Position>(t))
{ {
return; weapon.currentTarget = std::nullopt;
} }
else
{
const float distanceSquared = (ownPos.value - admin.get<Position>(t).value).lengthSquared();
if (distanceSquared > weapon.range * weapon.range)
{
weapon.currentTarget = std::nullopt;
}
}
}
const entt::entity targetEntity = *threat.currentTarget; // Acquire a new target if needed (nearest opposing-faction ship).
if (!admin.isValid(targetEntity) || !admin.hasAll<Position>(targetEntity)) if (!weapon.currentTarget)
{ {
return; float bestDistanceSquared = weapon.range * weapon.range;
} admin.forEach<ShipIdentity, Position, Faction>(
[&](entt::entity candidate, const ShipIdentity& /*si*/,
const QVector2D targetPos = admin.get<Position>(targetEntity).value; const Position& candidatePos, const Faction& candidateFaction)
const float dist = (pos.value - targetPos).length(); {
if (dist > weapon.range) const bool isValidTarget = ownFaction.isEnemy
{ ? !candidateFaction.isEnemy
return; : candidateFaction.isEnemy;
} if (!isValidTarget)
{
m_pendingDamage.push_back({targetEntity, weapon.damage, return;
currentTick + kWeaponImpactDelayTicks}); }
const float distanceSquared = (candidatePos.value - ownPos.value).lengthSquared();
FireEvent evt; if (distanceSquared < bestDistanceSquared)
evt.shooter = shipEntity; {
evt.target = targetEntity; bestDistanceSquared = distanceSquared;
evt.emittedAt = currentTick; weapon.currentTarget = candidate;
out.push_back(evt); }
});
weapon.cooldownTicks = static_cast<float>(kTickRateHz) / weapon.fireRateHz; }
}
void CombatSystem::resolveStationWeapon(entt::entity stationEntity,
StationWeapon& weapon,
const Position& stationPos,
const Faction& stationFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out)
{
// Validate or clear existing target.
if (weapon.currentTarget)
{
const entt::entity t = *weapon.currentTarget;
if (!admin.isValid(t) || !admin.hasAll<Position>(t))
{
weapon.currentTarget = std::nullopt;
}
else
{
const float dist = (stationPos.value - admin.get<Position>(t).value).length();
if (dist > weapon.range)
{
weapon.currentTarget = std::nullopt;
}
}
}
// Acquire a new target if needed (nearest opposing-faction ship).
if (!weapon.currentTarget)
{
float bestDist = weapon.range;
admin.forEach<ShipIdentity, Position, Faction>(
[&](entt::entity candidate, const ShipIdentity& /*si*/,
const Position& candidatePos, const Faction& candidateFaction)
{
const bool isValidTarget = stationFaction.isEnemy
? !candidateFaction.isEnemy
: candidateFaction.isEnemy;
if (!isValidTarget)
{
return;
}
const float dist = (candidatePos.value - stationPos.value).length();
if (dist < bestDist)
{
bestDist = dist;
weapon.currentTarget = candidate;
}
});
}
if (!weapon.currentTarget) if (!weapon.currentTarget)
{ {
return; return;
} }
if (weapon.cooldownTicks > 0.0f)
{
weapon.cooldownTicks -= 1.0f;
}
if (weapon.cooldownTicks > 0.0f)
{
return;
}
const entt::entity targetEntity = *weapon.currentTarget; const entt::entity targetEntity = *weapon.currentTarget;
if (!admin.isValid(targetEntity) || !admin.hasAll<Position>(targetEntity))
{
weapon.currentTarget = std::nullopt;
return;
}
const QVector2D targetPos = admin.get<Position>(targetEntity).value;
if ((stationPos.value - targetPos).length() > weapon.range)
{
return;
}
m_pendingDamage.push_back({targetEntity, weapon.damage, m_pendingDamage.push_back({targetEntity, weapon.damage,
currentTick + kWeaponImpactDelayTicks}); currentTick + kWeaponImpactDelayTicks});
FireEvent evt; FireEvent evt;
evt.shooter = stationEntity; evt.shooter = shipEntity;
evt.target = targetEntity; evt.target = targetEntity;
evt.emittedAt = currentTick; evt.emittedAt = currentTick;
out.push_back(evt); out.push_back(evt);
@@ -183,3 +128,4 @@ void CombatSystem::applyPendingDamage(Tick currentTick, EntityAdmin& admin)
} }
} }
} }

View File

@@ -39,19 +39,15 @@ private:
std::vector<PendingDamage> m_pendingDamage; std::vector<PendingDamage> m_pendingDamage;
void resolveShipWeapon(entt::entity shipEntity, Weapon& weapon, void resolveWeapon(
const ThreatResponse& threat, entt::entity shipEntity,
const Position& pos, Tick currentTick, Weapon& weapon,
EntityAdmin& admin, const Position& ownPos,
std::vector<FireEvent>& out); const Faction& ownFaction,
Tick currentTick,
void resolveStationWeapon(entt::entity stationEntity, EntityAdmin& admin,
StationWeapon& weapon, std::vector<FireEvent>& out);
const Position& stationPos,
const Faction& stationFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out);
const GameConfig& m_config; const GameConfig& m_config;
}; };

View File

@@ -224,7 +224,7 @@ void Simulation::placeInitialStructures()
const float psHp = static_cast<float>( const float psHp = static_cast<float>(
m_config.stations.playerStation.hpFormula.evaluate(psLevel)); m_config.stations.playerStation.hpFormula.evaluate(psLevel));
StationWeapon psWeapon; Weapon psWeapon;
psWeapon.damage = static_cast<float>( psWeapon.damage = static_cast<float>(
m_config.stations.playerStation.damageFormula.evaluate(psLevel)); m_config.stations.playerStation.damageFormula.evaluate(psLevel));
psWeapon.range = static_cast<float>( psWeapon.range = static_cast<float>(
@@ -246,7 +246,7 @@ void Simulation::placeInitialStructures()
} }
m_playerStation1Entity = m_admin.spawnStation( m_playerStation1Entity = m_admin.spawnStation(
anchor, psParsed.footprint, absCells, psHp, psHp, false); anchor, psParsed.footprint, absCells, psHp, psHp, false);
m_admin.addComponent<StationWeapon>(m_playerStation1Entity, psWeapon); m_admin.addComponent<Weapon>(m_playerStation1Entity, psWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId()); m_buildingSystem->registerTileOccupancy(absCells, allocateId());
} }
{ {
@@ -258,7 +258,7 @@ void Simulation::placeInitialStructures()
} }
m_playerStation2Entity = m_admin.spawnStation( m_playerStation2Entity = m_admin.spawnStation(
anchor, psParsed.footprint, absCells, psHp, psHp, false); anchor, psParsed.footprint, absCells, psHp, psHp, false);
m_admin.addComponent<StationWeapon>(m_playerStation2Entity, psWeapon); m_admin.addComponent<Weapon>(m_playerStation2Entity, psWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId()); m_buildingSystem->registerTileOccupancy(absCells, allocateId());
} }
@@ -285,7 +285,7 @@ void Simulation::placeEnemyStationSet(int generation)
const float esHp = static_cast<float>( const float esHp = static_cast<float>(
m_config.stations.enemyStation.hpFormula.evaluate(genD)); m_config.stations.enemyStation.hpFormula.evaluate(genD));
StationWeapon esWeapon; Weapon esWeapon;
esWeapon.damage = static_cast<float>( esWeapon.damage = static_cast<float>(
m_config.stations.enemyStation.damageFormula.evaluate(genD)); m_config.stations.enemyStation.damageFormula.evaluate(genD));
esWeapon.range = static_cast<float>( esWeapon.range = static_cast<float>(
@@ -307,7 +307,7 @@ void Simulation::placeEnemyStationSet(int generation)
} }
m_currentEnemyStationEntities[0] = m_admin.spawnStation( m_currentEnemyStationEntities[0] = m_admin.spawnStation(
anchor, esParsed.footprint, absCells, esHp, esHp, true); anchor, esParsed.footprint, absCells, esHp, esHp, true);
m_admin.addComponent<StationWeapon>(m_currentEnemyStationEntities[0], esWeapon); m_admin.addComponent<Weapon>(m_currentEnemyStationEntities[0], esWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId()); m_buildingSystem->registerTileOccupancy(absCells, allocateId());
} }
{ {
@@ -319,7 +319,7 @@ void Simulation::placeEnemyStationSet(int generation)
} }
m_currentEnemyStationEntities[1] = m_admin.spawnStation( m_currentEnemyStationEntities[1] = m_admin.spawnStation(
anchor, esParsed.footprint, absCells, esHp, esHp, true); anchor, esParsed.footprint, absCells, esHp, esHp, true);
m_admin.addComponent<StationWeapon>(m_currentEnemyStationEntities[1], esWeapon); m_admin.addComponent<Weapon>(m_currentEnemyStationEntities[1], esWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId()); m_buildingSystem->registerTileOccupancy(absCells, allocateId());
} }
} }

View File

@@ -165,9 +165,9 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
const Simulation sim(loadConfig(), 42); const Simulation sim(loadConfig(), 42);
int armedPlayerStations = 0; int armedPlayerStations = 0;
sim.admin().forEach<StationBody, Faction, StationWeapon>( sim.admin().forEach<StationBody, Faction, Weapon>(
[&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, [&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f,
const StationWeapon& w) const Weapon& w)
{ {
if (!f.isEnemy) if (!f.isEnemy)
{ {
@@ -185,9 +185,9 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
const Simulation sim(loadConfig(), 42); const Simulation sim(loadConfig(), 42);
int armedEnemyStations = 0; int armedEnemyStations = 0;
sim.admin().forEach<StationBody, Faction, StationWeapon>( sim.admin().forEach<StationBody, Faction, Weapon>(
[&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, [&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f,
const StationWeapon& w) const Weapon& w)
{ {
if (f.isEnemy) if (f.isEnemy)
{ {