change repair_tool application and add beams for salvager and repair_tool

This commit is contained in:
2026-06-18 22:14:09 +02:00
parent 7924e037aa
commit 9573b9789a
37 changed files with 498 additions and 199 deletions

View File

@@ -10,8 +10,6 @@
#include "tracing.h"
#include "WeaponComponent.h"
static constexpr Tick kWeaponImpactDelayTicks = 5;
CombatSystem::CombatSystem(const GameConfig& config)
: m_config(config)
{
@@ -20,7 +18,7 @@ CombatSystem::CombatSystem(const GameConfig& config)
void CombatSystem::tick(Tick currentTick,
EntityAdmin& admin,
BuildingSystem& /*buildings*/,
std::vector<WeaponFiredEvent>& outWeaponFiredEvents)
std::vector<BeamFiredEvent>& outBeamFiredEvents)
{
TRACE();
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
@@ -31,7 +29,7 @@ void CombatSystem::tick(Tick currentTick,
{
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outWeaponFiredEvents);
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outBeamFiredEvents);
});
}
@@ -42,7 +40,7 @@ void CombatSystem::resolveWeapon(
const FactionComponent& ownFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<WeaponFiredEvent>& out)
std::vector<BeamFiredEvent>& out)
{
if (weapon.cooldownTicks > 0.0f)
{
@@ -109,9 +107,10 @@ void CombatSystem::resolveWeapon(
const entt::entity targetEntity = *weapon.currentTarget;
m_pendingDamage.push_back({targetEntity, weapon.damage,
currentTick + kWeaponImpactDelayTicks});
currentTick + kBeamImpactDelayTicks});
WeaponFiredEvent evt;
BeamFiredEvent evt;
evt.kind = BeamKind::Weapon;
evt.shooter = shipEntity;
evt.target = targetEntity;
evt.emittedAt = currentTick;

View File

@@ -7,7 +7,7 @@
#include "Building.h"
#include "FactionComponent.h"
#include "WeaponFiredEvent.h"
#include "BeamFiredEvent.h"
#include "GameConfig.h"
#include "PositionComponent.h"
#include "Tick.h"
@@ -26,7 +26,7 @@ public:
void tick(Tick currentTick,
EntityAdmin& admin,
BuildingSystem& buildings,
std::vector<WeaponFiredEvent>& outWeaponFiredEvents);
std::vector<BeamFiredEvent>& outBeamFiredEvents);
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
@@ -47,7 +47,7 @@ private:
const FactionComponent& ownFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<WeaponFiredEvent>& out);
std::vector<BeamFiredEvent>& out);
const GameConfig& m_config;
};

View File

@@ -19,52 +19,91 @@ RepairSystem::RepairSystem(EntityAdmin& admin)
{
}
void RepairSystem::tick()
void RepairSystem::tick(Tick currentTick, std::vector<BeamFiredEvent>& outBeamFiredEvents)
{
TRACE();
// Apply heals whose mid-beam delay has elapsed (cycles started on prior ticks).
applyPendingHeals(currentTick);
const std::vector<RepairableInfo> repairables = buildRepairables(m_admin);
m_admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
[&](entt::entity /*re*/, RepairToolComponent& tool, const ModuleOwnerComponent& owner)
{
if (tool.cooldownTicksRemaining > 0) { --tool.cooldownTicksRemaining; }
if (tool.cooldownTicksRemaining > 0) { return; }
if (tool.repairIntervalTicks <= 0) { return; }
if (!m_admin.hasAll<PositionComponent>(owner.owner)) { return; }
const QVector2D ownerPos = m_admin.get<PositionComponent>(owner.owner).value;
// Honour the executor-set target if it is still valid and in range.
// Choose a target: honour the executor-set target if it is still valid
// and in range, else fall back to the nearest damaged friendly in range.
std::optional<entt::entity> target;
if (tool.currentTarget)
{
const entt::entity t = *tool.currentTarget;
if (m_admin.isValid(t) && m_admin.hasAll<HealthComponent, PositionComponent>(t))
{
HealthComponent& th = m_admin.get<HealthComponent>(t);
const HealthComponent& th = m_admin.get<HealthComponent>(t);
const float dist =
(m_admin.get<PositionComponent>(t).value - ownerPos).length();
if (th.hp > 0.0f && th.hp < th.maxHp && dist <= tool.range_tiles)
{
th.hp = std::min(th.hp + tool.ratePerTick, th.maxHp);
return;
target = t;
}
}
}
// Fallback: heal the nearest damaged friendly within tool range.
tool.currentTarget = std::nullopt;
float bestDist = tool.range_tiles;
for (const RepairableInfo& r : repairables)
if (!target)
{
if (r.isEnemy) { continue; }
if (r.hp <= 0.0f || r.hp >= r.maxHp) { continue; }
const float dist = (r.position - ownerPos).length();
if (dist < bestDist)
tool.currentTarget = std::nullopt;
float bestDist = tool.range_tiles;
for (const RepairableInfo& r : repairables)
{
bestDist = dist;
tool.currentTarget = r.entity;
if (r.isEnemy) { continue; }
if (r.hp <= 0.0f || r.hp >= r.maxHp) { continue; }
const float dist = (r.position - ownerPos).length();
if (dist < bestDist)
{
bestDist = dist;
tool.currentTarget = r.entity;
}
}
target = tool.currentTarget;
}
if (!tool.currentTarget) { return; }
if (!target) { return; }
HealthComponent& targetHealth = m_admin.get<HealthComponent>(*tool.currentTarget);
targetHealth.hp = std::min(targetHealth.hp + tool.ratePerTick, targetHealth.maxHp);
// Start a repair cycle: emit the beam now, apply the heal mid-beam, and
// begin the cooldown at cycle start (not at effect application).
outBeamFiredEvents.push_back(
BeamFiredEvent{BeamKind::Repair, owner.owner, *target, currentTick});
m_pendingHeals.push_back({*target, tool.repairAmountHp,
currentTick + kBeamImpactDelayTicks});
tool.cooldownTicksRemaining = tool.repairIntervalTicks;
});
}
void RepairSystem::applyPendingHeals(Tick currentTick)
{
std::vector<PendingHeal>::iterator it = m_pendingHeals.begin();
while (it != m_pendingHeals.end())
{
if (it->appliesAt <= currentTick)
{
if (m_admin.isValid(it->target) && m_admin.hasAll<HealthComponent>(it->target))
{
HealthComponent& h = m_admin.get<HealthComponent>(it->target);
if (h.hp > 0.0f && h.hp < h.maxHp)
{
h.hp = std::min(h.hp + it->amountHp, h.maxHp);
}
}
it = m_pendingHeals.erase(it);
}
else
{
++it;
}
}
}

View File

@@ -1,17 +1,36 @@
#pragma once
#include <vector>
#include "BeamFiredEvent.h"
#include "Tick.h"
#include "entt/entity/entity.hpp"
class EntityAdmin;
// World-mutation system for repair modules: validates each tool's target (set by
// RepairExecutor), falls back to the nearest damaged friendly in range, and
// applies healing. Runs every tick, independent of behavior selection.
// World-mutation system for repair modules: each tool runs a cycle on its own
// cooldown. When a cycle starts it picks a target (the RepairExecutor-set target,
// else the nearest damaged friendly in range), emits a repair beam, and schedules
// the heal for mid-beam (kBeamImpactDelayTicks later) — mirroring weapon firing.
// Runs every tick, independent of behavior selection.
class RepairSystem
{
public:
explicit RepairSystem(EntityAdmin& admin);
void tick();
void tick(Tick currentTick, std::vector<BeamFiredEvent>& outBeamFiredEvents);
private:
EntityAdmin& m_admin;
struct PendingHeal
{
entt::entity target;
float amountHp;
Tick appliesAt;
};
void applyPendingHeals(Tick currentTick);
EntityAdmin& m_admin;
std::vector<PendingHeal> m_pendingHeals;
};

View File

@@ -8,9 +8,12 @@
#include "BuildingSystem.h"
#include "DeliverScrapBehavior.h"
#include "EntityAdmin.h"
#include <map>
#include "ModuleOwnerComponent.h"
#include "PositionComponent.h"
#include "SalvageCargoComponent.h"
#include "ScrapDataComponent.h"
#include "ScrapSystem.h"
#include "tracing.h"
@@ -19,9 +22,13 @@ SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
{
}
void SalvagerSystem::tick(ScrapSystem& scraps, BuildingSystem& buildings)
void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem& buildings,
std::vector<BeamFiredEvent>& outBeamFiredEvents)
{
TRACE();
// Apply collections whose mid-beam delay has elapsed (cycles started earlier).
applyPendingCollections(currentTick, scraps);
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
// Tick down per-module collection cooldowns.
@@ -31,23 +38,39 @@ void SalvagerSystem::tick(ScrapSystem& scraps, BuildingSystem& buildings)
if (c.cooldownTicksRemaining > 0) { --c.cooldownTicksRemaining; }
});
// Collection: each ready, in-range module collects one scrap.
// 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;
for (const PendingCollection& pc : m_pendingCollections)
{
++claimedUnits[pc.scrap];
}
// 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 /*ce*/, SalvageCargoComponent& c, const ModuleOwnerComponent& o)
[&](entt::entity moduleEntity, SalvageCargoComponent& c, 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; }
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 (scraps.consume(si.entity))
if (claimedUnits[si.entity] >= m_admin.get<ScrapDataComponent>(si.entity).amount)
{
++c.current;
c.cooldownTicksRemaining = c.collectionIntervalTicks;
break;
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,
currentTick + kBeamImpactDelayTicks});
++claimedUnits[si.entity];
c.cooldownTicksRemaining = c.collectionIntervalTicks;
break;
}
});
@@ -77,3 +100,27 @@ void SalvagerSystem::tick(ScrapSystem& scraps, BuildingSystem& buildings)
});
});
}
void SalvagerSystem::applyPendingCollections(Tick currentTick, ScrapSystem& scraps)
{
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
while (it != m_pendingCollections.end())
{
if (it->appliesAt <= currentTick)
{
if (m_admin.isValid(it->module) && m_admin.hasAll<SalvageCargoComponent>(it->module))
{
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(it->module);
if (c.current < c.capacity && scraps.collectOne(it->scrap))
{
++c.current;
}
}
it = m_pendingCollections.erase(it);
}
else
{
++it;
}
}
}

View File

@@ -1,19 +1,39 @@
#pragma once
#include <vector>
#include "BeamFiredEvent.h"
#include "Tick.h"
#include "entt/entity/entity.hpp"
class BuildingSystem;
class EntityAdmin;
class ScrapSystem;
// World-mutation system for salvage modules: collects scrap into cargo and
// delivers full cargo at a SalvageBay. Runs every tick, independent of which
// behavior the AiSystem selected.
// World-mutation system for salvage modules: each module runs a collection cycle
// on its own cooldown. When a cycle starts it emits a salvage beam toward an
// in-range scrap pile and schedules the collection of one scrap for mid-beam
// (kBeamImpactDelayTicks later) — mirroring weapon firing. Also delivers full
// cargo at a SalvageBay. Runs every tick, independent of behavior selection.
class SalvagerSystem
{
public:
explicit SalvagerSystem(EntityAdmin& admin);
void tick(ScrapSystem& scraps, BuildingSystem& buildings);
void tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem& buildings,
std::vector<BeamFiredEvent>& outBeamFiredEvents);
private:
EntityAdmin& m_admin;
struct PendingCollection
{
entt::entity module;
entt::entity scrap;
Tick appliesAt;
};
void applyPendingCollections(Tick currentTick, ScrapSystem& scraps);
EntityAdmin& m_admin;
std::vector<PendingCollection> m_pendingCollections;
};

View File

@@ -46,6 +46,25 @@ std::optional<int> ScrapSystem::consume(entt::entity entity)
return amount;
}
bool ScrapSystem::collectOne(entt::entity entity)
{
if (!m_admin.isValid(entity) || !m_admin.hasAll<ScrapDataComponent>(entity))
{
return false;
}
ScrapDataComponent& data = m_admin.get<ScrapDataComponent>(entity);
if (data.amount <= 0)
{
return false;
}
--data.amount;
if (data.amount <= 0)
{
m_admin.destroy(entity);
}
return true;
}
std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const
{
std::vector<ScrapInfo> result;

View File

@@ -28,6 +28,11 @@ public:
// Removes the scrap and returns its amount, or nullopt if not found.
std::optional<int> consume(entt::entity entity);
// Collects a single scrap unit from the pile: decrements its amount by one,
// destroying the entity once depleted. Returns true if a scrap was collected,
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
bool collectOne(entt::entity entity);
// Lightweight snapshot for callers that need to iterate all scrap.
std::vector<ScrapInfo> allScrapInfo() const;

View File

@@ -157,9 +157,14 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
if (modDef->repairCapability)
{
RepairToolComponent rt;
rt.ratePerTick = static_cast<float>(
modDef->repairCapability->repairRateFormula.evaluate(mx))
/ static_cast<float>(kTickRateHz);
const double repairRateHz =
modDef->repairCapability->repairRateFormula.evaluate(mx);
rt.repairIntervalTicks = (repairRateHz > 0.0)
? static_cast<int>(kTickRateHz / repairRateHz + 0.5)
: 0;
rt.repairAmountHp = static_cast<float>(
modDef->repairCapability->repairAmountHpFormula.evaluate(mx));
rt.cooldownTicksRemaining = 0;
rt.range_tiles = static_cast<float>(
modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize;
rt.currentTarget = std::nullopt;
@@ -321,8 +326,15 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
for (entt::entity child : repairChildren)
{
RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child);
applyMod(rt.ratePerTick, "repair_rate", repairMods);
applyMod(rt.range_tiles, "repair_range", repairMods);
// Apply rate modifier: compute cycles/s from interval, apply, convert back.
float fRate = (rt.repairIntervalTicks > 0)
? static_cast<float>(kTickRateHz) / static_cast<float>(rt.repairIntervalTicks)
: 0.0f;
applyMod(fRate, "repair_rate", repairMods);
applyMod(rt.range_tiles, "repair_range", repairMods);
rt.repairIntervalTicks = (fRate > 0.0f)
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
: 0;
}
// --- Pass 3: attach behavior components based on capability presence -----