Rename scrap-drop entity to "debris" in code and config

Implements the requirements rename in the codebase. The salvageable object
dropped by destroyed ships and defence stations is now the "debris" entity;
the scrap resource it yields (cargo, delivery, smelting, threat,
scrap_drop_formula, scrap_per_threat) is unchanged.

Entity renames: ScrapDataComponent->DebrisComponent, ScrapSystem->DebrisSystem,
ScrapInfo->DebrisInfo, ScrapSelectionChangedEvent->DebrisSelectionChangedEvent
(member scrap->debris), spawnScrap->spawnDebris, getScraps->getDebrisSystem,
getAllScrapInfo->getAllDebrisInfo, scrapAtWorldPos/scrapInBox->debris*,
SalvageScrapBehavior::scrapTarget->debrisTarget, PendingCollection::scrap->debris,
ScrapTest.cpp->DebrisTest.cpp. Config key scrap_despawn_seconds->
debris_despawn_seconds (WorldConfig.scrapDespawnSeconds->debrisDespawnSeconds).
The salvage/deliver behavior classes keep their names (they act on the scrap
resource).

Also implements the new REQ-UI-DEBRIS-PANEL behavior in SelectedBuildingPanel:
a single selected piece of debris shows a "Debris" heading plus a "Scrap" stat
row; a multi/mixed field selection appends "Debris x N" and "Scrap x N" lines.

Updates the four renamed REQ-ID references in comments/docs, plus architecture.md
and derived.md. All 449 test cases pass; app, tests, and balancing targets build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
2026-07-23 13:10:04 +02:00
parent cccc464b43
commit 51e45e10a1
47 changed files with 450 additions and 425 deletions

View File

@@ -4,7 +4,7 @@
# a fresh player defence station holds one early parity wave unaided; the # a fresh player defence station holds one early parity wave unaided; the
# enemy station at level 0 matches the player station exactly and scales # enemy station at level 0 matches the player station exactly and scales
# with the push level x. Station scrap drops stay authored (pushing rewards # with the push level x. Station scrap drops stay authored (pushing rewards
# are tuned independently of ship production costs, REQ-RES-SCRAP-DROP). # are tuned independently of ship production costs, REQ-RES-DEBRIS-DROP).
[hq] [hq]
surface_mask = [ surface_mask = [

View File

@@ -3,7 +3,7 @@ height_tiles = 40
refund_percentage = 100 refund_percentage = 100
deconstruction_time_seconds = 0.1 deconstruction_time_seconds = 0.1
starting_building_blocks = 200 starting_building_blocks = 200
scrap_despawn_seconds = 120 debris_despawn_seconds = 120
scrap_per_threat = 0.25 scrap_per_threat = 0.25
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20

View File

@@ -3,7 +3,7 @@ height_tiles = 60
refund_percentage = 75 refund_percentage = 75
deconstruction_time_seconds = 0.1 deconstruction_time_seconds = 0.1
starting_building_blocks = 100 starting_building_blocks = 100
scrap_despawn_seconds = 30 debris_despawn_seconds = 30
scrap_per_threat = 1.0 scrap_per_threat = 1.0
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20

View File

@@ -25,7 +25,7 @@ The simulation advances in discrete ticks. All game quantities — production ti
- Tick rate: fixed at 30 Hz; `tickDurationMs = 1000 / 30 ≈ 33.33`. - Tick rate: fixed at 30 Hz; `tickDurationMs = 1000 / 30 ≈ 33.33`.
- Ticks are driven by an accumulator that is independent of the render rate. Each render frame, the driver adds `elapsedWallMs × gameSpeedMultiplier` to an accumulator and flushes one `tick()` per `tickDurationMs` of accumulated time (so multiple sim ticks may run between frames at high speeds, or a frame may run no ticks at low speeds). `gameSpeedMultiplier` ∈ {0, 0.5, 1, 2, 4} per REQ-UI-SPEED; 0× freezes the accumulator (pause). The concrete driver lives in the Rendering section. - Ticks are driven by an accumulator that is independent of the render rate. Each render frame, the driver adds `elapsedWallMs × gameSpeedMultiplier` to an accumulator and flushes one `tick()` per `tickDurationMs` of accumulated time (so multiple sim ticks may run between frames at high speeds, or a frame may run no ticks at low speeds). `gameSpeedMultiplier` ∈ {0, 0.5, 1, 2, 4} per REQ-UI-SPEED; 0× freezes the accumulator (pause). The concrete driver lives in the Rendering section.
- Config-level durations given in seconds (recipe durations, wave gap ranges, scrap despawn, etc.) are converted to ticks at config-load time. - Config-level durations given in seconds (recipe durations, wave gap ranges, debris despawn, etc.) are converted to ticks at config-load time.
Consequences: determinism, replayability, and the time-scale feature fall out for free. The simulation advances the same number of ticks over the same amount of game-time regardless of whether the game renders at 60 FPS, 30 FPS, or a stuttery mix. Consequences: determinism, replayability, and the time-scale feature fall out for free. The simulation advances the same number of ticks over the same amount of game-time regardless of whether the game renders at 60 FPS, 30 FPS, or a stuttery mix.
@@ -44,7 +44,7 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
- Tile coordinates are `QPoint(x, y)`. Origin `(0, 0)` is the first space tile (just right of the asteroid's right edge at game start). X grows right; Y grows down. - Tile coordinates are `QPoint(x, y)`. Origin `(0, 0)` is the first space tile (just right of the asteroid's right edge at game start). X grows right; Y grows down.
- Asteroid tiles have `x < 0`. Asteroid left-expansions add tiles at increasingly negative X; the origin never shifts, so existing tile coordinates remain stable across expansions. - Asteroid tiles have `x < 0`. Asteroid left-expansions add tiles at increasingly negative X; the origin never shifts, so existing tile coordinates remain stable across expansions.
- Continuous world positions (ship centers, scrap drops, projectiles) use `QVector2D` in tile units — one tile = 1.0 world unit. A ship center at `QVector2D(-3.5, 4.0)` sits at the center of the tile 3.5 tiles left of the asteroid's right edge and 4 tiles down from the top. - Continuous world positions (ship centers, debris, projectiles) use `QVector2D` in tile units — one tile = 1.0 world unit. A ship center at `QVector2D(-3.5, 4.0)` sits at the center of the tile 3.5 tiles left of the asteroid's right edge and 4 tiles down from the top.
- Rendering multiplies world units by the tile size in pixels (20) at draw time. - Rendering multiplies world units by the tile size in pixels (20) at draw time.
- Ship position always refers to the ship's center — this is the point used for sensor, attack-range, and hit-detection checks. - Ship position always refers to the ship's center — this is the point used for sensor, attack-range, and hit-detection checks.
@@ -52,7 +52,7 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
Simulation types shared across subsystems: Simulation types shared across subsystems:
- `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, scrap drops, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `AttackBehavior.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly. - `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, debris, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `AttackBehavior.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly.
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed. - `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem). - `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem).
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap). - `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
@@ -115,9 +115,9 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER). 6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then the `AiSystem` runs three batched phases: every behavior **evaluator** scores its behavior and sets its target data; a **selection** pass records the highest-scoring behavior per ship in `SelectedBehaviorComponent`; each behavior **executor** runs for the winner, writing `MovementIntent` and preferred module targets. The module systems then perform world mutation: `SalvagerSystem` (scrap collection/delivery) and `RepairSystem` (healing). See Movement Arbitration. 7. **Ship behavior systems** — clear `MovementIntent` on each ship, then the `AiSystem` runs three batched phases: every behavior **evaluator** scores its behavior and sets its target data; a **selection** pass records the highest-scoring behavior per ship in `SelectedBehaviorComponent`; each behavior **executor** runs for the winner, writing `MovementIntent` and preferred module targets. The module systems then perform world mutation: `SalvagerSystem` (scrap collection/delivery) and `RepairSystem` (healing). See Movement Arbitration.
8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `BeamFiredEvent` to the sim's beam-fired-event queue (REQ-SHP-FIRING-BEAM). The repair and salvage module systems (tick step 7d) append their own `BeamFiredEvent`s to the same queue when they start a cycle. 8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `BeamFiredEvent` to the sim's beam-fired-event queue (REQ-SHP-FIRING-BEAM). The repair and salvage module systems (tick step 7d) append their own `BeamFiredEvent`s to the same queue when they start a cycle.
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities. 9. **Deaths & loot** — process queued deaths: drop debris (REQ-RES-DEBRIS-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities.
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`. 10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP). 11. **Debris despawn** — decrement debris timers; remove expired debris (REQ-RES-DEBRIS-DROP).
## CMake Target Layout ## CMake Target Layout
@@ -193,20 +193,21 @@ struct Building {
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances. - Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components. - No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
## Scrap ## Debris
Scrap is the only non-ship, non-building entity in the simulation: Debris the salvageable object dropped by destroyed ships and defence stations — is the
only non-ship, non-building entity in the simulation. Each piece carries a scrap amount:
```cpp ```cpp
struct Scrap { struct Debris {
EntityId id; EntityId id;
QVector2D position; // world units, tile-fractional; ship-center convention QVector2D position; // world units, tile-fractional; ship-center convention
int amount; int amount; // scrap the piece still holds
Tick despawnAt; // absolute tick at which the scrap is removed Tick despawnAt; // absolute tick at which the debris is removed
}; };
``` ```
Created in tick step 9 (Deaths & loot) per REQ-RES-SCRAP-DROP, consumed by salvage ships in tick step 7 (ScrapCollector), and removed in tick step 11 when the current tick reaches `despawnAt`. Created in tick step 9 (Deaths & loot) per REQ-RES-DEBRIS-DROP, drained one scrap per cycle by salvage ships in tick step 7 (SalvagerSystem), and removed in tick step 11 when the current tick reaches `despawnAt`.
## Ships ## Ships
@@ -234,7 +235,7 @@ struct RetreatBehavior { float retreatHpFraction; QVector2D retreatPoint;
struct AttackBehavior { std::optional<EntityId> currentTarget; float score; }; struct AttackBehavior { std::optional<EntityId> currentTarget; float score; };
struct RepairBehavior { std::optional<EntityId> currentTarget; struct RepairBehavior { std::optional<EntityId> currentTarget;
float maxRepairRange_tiles; float score; }; float maxRepairRange_tiles; float score; };
struct SalvageScrapBehavior { std::optional<QVector2D> scrapTarget; struct SalvageScrapBehavior { std::optional<QVector2D> debrisTarget;
float maxCollectionRange_tiles; float score; }; float maxCollectionRange_tiles; float score; };
struct DeliverScrapBehavior { BuildingId deliveryBay; float score; }; struct DeliverScrapBehavior { BuildingId deliveryBay; float score; };
struct SelectedBehaviorComponent { BehaviorKind winner; float bestScore; }; // selection result struct SelectedBehaviorComponent { BehaviorKind winner; float bestScore; }; // selection result

View File

@@ -17,7 +17,7 @@ move. Combat stats were tuned empirically against the arena suite in
- Reprocessing: 4 scrap per cycle, 4 s; full-pool weights iron_ingot 30 / - Reprocessing: 4 scrap per cycle, 4 s; full-pool weights iron_ingot 30 /
copper_ingot 30 / silicon 20 / voidsteel 20 → threat(voidsteel) copper_ingot 30 / silicon 20 / voidsteel 20 → threat(voidsteel)
= (4·4 + 4)/0.2 = 100. = (4·4 + 4)/0.2 = 100.
- `scrap_despawn_seconds = 120` (a capital kill drops hundreds of scrap, - `debris_despawn_seconds = 120` (a capital kill drops hundreds of scrap,
collected one per salvage cycle). collected one per salvage cycle).
## Recipes and item threats ## Recipes and item threats

View File

@@ -22,7 +22,7 @@
#include "PositionComponent.h" #include "PositionComponent.h"
#include "RepairSystem.h" #include "RepairSystem.h"
#include "SalvagerSystem.h" #include "SalvagerSystem.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "ShipsConfig.h" #include "ShipsConfig.h"
@@ -63,7 +63,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
m_movementIntentSystem = std::make_unique<MovementIntentSystem>(); m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>(); m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig); m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin); m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin); m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
m_repairSystem = std::make_unique<RepairSystem>(m_admin); m_repairSystem = std::make_unique<RepairSystem>(m_admin);
@@ -322,9 +322,9 @@ void ArenaSimulation::tick()
// Ship behavior systems (tick step 7): evaluate, select winner, execute. // Ship behavior systems (tick step 7): evaluate, select winner, execute.
// Module + combat systems emit their tool beams into a shared buffer. // Module + combat systems emit their tool beams into a shared buffer.
m_shipSystem->clearMovementIntents(); m_shipSystem->clearMovementIntents();
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_scrapSystem); m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
std::vector<BeamFiredEvent> beamFiredEvents; std::vector<BeamFiredEvent> beamFiredEvents;
m_salvagerSystem->tick(m_currentTick, *m_scrapSystem, *m_buildingSystem, beamFiredEvents); m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, beamFiredEvents);
m_repairSystem->tick(m_currentTick, beamFiredEvents); m_repairSystem->tick(m_currentTick, beamFiredEvents);
// Combat resolution (tick step 8). // Combat resolution (tick step 8).
@@ -340,7 +340,7 @@ void ArenaSimulation::tick()
m_dynamicBodySystem->tick(m_admin); m_dynamicBodySystem->tick(m_admin);
// Scrap despawn (tick step 11). // Scrap despawn (tick step 11).
m_scrapSystem->tickDespawn(m_currentTick); m_debrisSystem->tickDespawn(m_currentTick);
++m_currentTick; ++m_currentTick;
@@ -371,8 +371,8 @@ void ArenaSimulation::tickDeaths()
if (si.scrapDrop > 0) if (si.scrapDrop > 0)
{ {
const Tick despawnAt = m_currentTick const Tick despawnAt = m_currentTick
+ secondsToTicks(m_gameConfig.world.scrapDespawnSeconds); + secondsToTicks(m_gameConfig.world.debrisDespawnSeconds);
m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt); m_debrisSystem->spawn(pos.value, si.scrapDrop, despawnAt);
} }
m_shipSystem->despawn(deadEntity); m_shipSystem->despawn(deadEntity);
} }
@@ -497,9 +497,9 @@ const ShipSystem& ArenaSimulation::getShips() const
return *m_shipSystem; return *m_shipSystem;
} }
const ScrapSystem& ArenaSimulation::getScraps() const const DebrisSystem& ArenaSimulation::getDebrisSystem() const
{ {
return *m_scrapSystem; return *m_debrisSystem;
} }
EntityAdmin& ArenaSimulation::getAdmin() EntityAdmin& ArenaSimulation::getAdmin()

View File

@@ -26,7 +26,7 @@ class MovementIntentSystem;
class RepairSystem; class RepairSystem;
class SalvagerSystem; class SalvagerSystem;
class ShipSystem; class ShipSystem;
class ScrapSystem; class DebrisSystem;
struct ArenaStatus struct ArenaStatus
{ {
@@ -86,7 +86,7 @@ public:
const ArenaConfig& getArenaConfig() const; const ArenaConfig& getArenaConfig() const;
const BuildingSystem& getBuildings() const; const BuildingSystem& getBuildings() const;
const ShipSystem& getShips() const; const ShipSystem& getShips() const;
const ScrapSystem& getScraps() const; const DebrisSystem& getDebrisSystem() const;
EntityAdmin& getAdmin(); EntityAdmin& getAdmin();
const EntityAdmin& getAdmin() const; const EntityAdmin& getAdmin() const;
@@ -114,7 +114,7 @@ private:
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem; std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem; std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
std::unique_ptr<CombatSystem> m_combatSystem; std::unique_ptr<CombatSystem> m_combatSystem;
std::unique_ptr<ScrapSystem> m_scrapSystem; std::unique_ptr<DebrisSystem> m_debrisSystem;
std::unique_ptr<SalvagerSystem> m_salvagerSystem; std::unique_ptr<SalvagerSystem> m_salvagerSystem;
std::unique_ptr<RepairSystem> m_repairSystem; std::unique_ptr<RepairSystem> m_repairSystem;

View File

@@ -24,11 +24,11 @@
#include "PositionComponent.h" #include "PositionComponent.h"
#include "RepairBehavior.h" #include "RepairBehavior.h"
#include "SalvageScrapBehavior.h" #include "SalvageScrapBehavior.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "SensorRangeComponent.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
namespace namespace
{ {
@@ -153,7 +153,7 @@ void ArenaView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
maxRadius = shorter / 2.0f; maxRadius = shorter / 2.0f;
} }
else if (m_sim->getAdmin().isValid(event->target) else if (m_sim->getAdmin().isValid(event->target)
&& m_sim->getAdmin().hasAll<ScrapDataComponent>(event->target)) && m_sim->getAdmin().hasAll<DebrisComponent>(event->target))
{ {
maxRadius = 0.1f; maxRadius = 0.1f;
} }
@@ -178,7 +178,7 @@ void ArenaView::paintGL()
drawTiles(painter); drawTiles(painter);
drawBuildings(painter); drawBuildings(painter);
drawStations(painter); drawStations(painter);
drawScrap(painter); drawDebris(painter);
if (m_debugDraw) if (m_debugDraw)
{ {
drawDebugSensorRanges(painter); drawDebugSensorRanges(painter);
@@ -336,12 +336,12 @@ void ArenaView::drawBuildings(QPainter& painter)
} }
} }
void ArenaView::drawScrap(QPainter& painter) void ArenaView::drawDebris(QPainter& painter)
{ {
const float r = getTilePx() * 0.2f; const float r = getTilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo()) for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
const QPointF center = worldToWidget(scrap.position); const QPointF center = worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90)); painter.setBrush(QColor(128, 110, 90));
painter.setPen(QPen(QColor(50, 40, 30), 1)); painter.setPen(QPen(QColor(50, 40, 30), 1));
painter.drawEllipse(center, painter.drawEllipse(center,
@@ -529,9 +529,9 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
const PositionComponent& pos, const FactionComponent& fac, const PositionComponent& pos, const FactionComponent& fac,
const SalvageScrapBehavior& salvage) const SalvageScrapBehavior& salvage)
{ {
if (!salvage.scrapTarget.has_value()) { return; } if (!salvage.debrisTarget.has_value()) { return; }
drawTargetLine(fac.isEnemy, pos.value, *salvage.scrapTarget); drawTargetLine(fac.isEnemy, pos.value, *salvage.debrisTarget);
}); });
} }

View File

@@ -50,7 +50,7 @@ private:
void drawTiles(QPainter& painter); void drawTiles(QPainter& painter);
void drawBuildings(QPainter& painter); void drawBuildings(QPainter& painter);
void drawStations(QPainter& painter); void drawStations(QPainter& painter);
void drawScrap(QPainter& painter); void drawDebris(QPainter& painter);
void drawShips(QPainter& painter); void drawShips(QPainter& painter);
void drawDebugSensorRanges(QPainter& painter); void drawDebugSensorRanges(QPainter& painter);
void drawDebugTargetLines(QPainter& painter); void drawDebugTargetLines(QPainter& painter);

View File

@@ -265,7 +265,7 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage")); cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds"); cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds");
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks")); cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds"); cfg.debrisDespawnSeconds = requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds");
cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat"); cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m"); cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m; cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;

View File

@@ -70,8 +70,8 @@ struct WorldConfig
int refundPercentage; // REQ-BLD-DECONSTRUCT int refundPercentage; // REQ-BLD-DECONSTRUCT
double deconstructionTimeSeconds; // REQ-BLD-DECON-QUEUE double deconstructionTimeSeconds; // REQ-BLD-DECON-QUEUE
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP double debrisDespawnSeconds; // REQ-RES-DEBRIS-DROP
double scrapPerThreat; // REQ-RES-SCRAP-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat) double scrapPerThreat; // REQ-RES-DEBRIS-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat)
double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE) double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE)
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config) double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR

View File

@@ -8,7 +8,7 @@
#include "HqProxyComponent.h" #include "HqProxyComponent.h"
#include "MovementIntentComponent.h" #include "MovementIntentComponent.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
#include "SensorRangeComponent.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
@@ -80,11 +80,11 @@ entt::entity EntityAdmin::spawnStation(QPoint anchor, QSize footprint,
return entity; return entity;
} }
entt::entity EntityAdmin::spawnScrap(QVector2D position, int amount, Tick despawnAt) entt::entity EntityAdmin::spawnDebris(QVector2D position, int amount, Tick despawnAt)
{ {
entt::entity entity = createEntity(); entt::entity entity = createEntity();
add<PositionComponent>(entity, PositionComponent{position}); add<PositionComponent>(entity, PositionComponent{position});
add<ScrapDataComponent>(entity, ScrapDataComponent{amount}); add<DebrisComponent>(entity, DebrisComponent{amount});
add<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt}); add<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
return entity; return entity;
} }

View File

@@ -62,7 +62,7 @@ public:
const std::vector<QPoint>& bodyCells, const std::vector<QPoint>& bodyCells,
float hp, float maxHp, bool isEnemy); float hp, float maxHp, bool isEnemy);
entt::entity spawnScrap(QVector2D position, int amount, Tick despawnAt); entt::entity spawnDebris(QVector2D position, int amount, Tick despawnAt);
entt::entity spawnHqProxy(QVector2D position, float hp, float maxHp); entt::entity spawnHqProxy(QVector2D position, float hp, float maxHp);

View File

@@ -20,7 +20,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/RetreatBehavior.h ${CMAKE_CURRENT_SOURCE_DIR}/RetreatBehavior.h
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/SalvagerComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/SalvageScrapBehavior.h ${CMAKE_CURRENT_SOURCE_DIR}/SalvageScrapBehavior.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapDataComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/DebrisComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBehaviorComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/SelectedBehaviorComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/SensorRangeComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/SensorRangeComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipIdentityComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipIdentityComponent.h

View File

@@ -0,0 +1,8 @@
#pragma once
// Marks a piece of debris and holds the amount of scrap it still contains
// (REQ-RES-DEBRIS-DROP). Salvage modules collect one scrap per cycle (REQ-SHP-SALVAGE).
struct DebrisComponent
{
int amount;
};

View File

@@ -5,10 +5,10 @@
#include <QVector2D> #include <QVector2D>
// Collect-scrap behavior (one half of the old SalvageBehaviorComponent). The // Collect-scrap behavior (one half of the old SalvageBehaviorComponent). The
// evaluator finds the nearest scrap and sets scrapTarget when cargo is not full. // evaluator finds the nearest debris and sets debrisTarget when cargo is not full.
struct SalvageScrapBehavior struct SalvageScrapBehavior
{ {
std::optional<QVector2D> scrapTarget; std::optional<QVector2D> debrisTarget;
float maxCollectionRange_tiles = 0.0f; float maxCollectionRange_tiles = 0.0f;
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
float score = 0.0f; float score = 0.0f;

View File

@@ -1,6 +0,0 @@
#pragma once
struct ScrapDataComponent
{
int amount;
};

View File

@@ -6,6 +6,6 @@ struct ShipIdentityComponent
{ {
std::string schematicId; std::string schematicId;
// Scrap dropped on destruction, derived from the ship's as-built threat cost // Scrap dropped on destruction, derived from the ship's as-built threat cost
// at spawn time (REQ-RES-SCRAP-DROP). // at spawn time (REQ-RES-DEBRIS-DROP).
int scrapDrop = 0; int scrapDrop = 0;
}; };

View File

@@ -43,7 +43,7 @@ AiSystem::AiSystem(const GameConfig& config)
} }
void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings, void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
const ScrapSystem& scraps) const DebrisSystem& debris)
{ {
TRACE(); TRACE();
@@ -54,7 +54,7 @@ void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
m_retreatEvaluator.evaluate(admin); m_retreatEvaluator.evaluate(admin);
m_attackEvaluator.evaluate(admin); m_attackEvaluator.evaluate(admin);
m_repairEvaluator.evaluate(admin); m_repairEvaluator.evaluate(admin);
m_salvageScrapEvaluator.evaluate(admin, scraps); m_salvageScrapEvaluator.evaluate(admin, debris);
m_deliverScrapEvaluator.evaluate(admin, buildings); m_deliverScrapEvaluator.evaluate(admin, buildings);
// Phase 2: pick the highest-scoring behavior per ship. // Phase 2: pick the highest-scoring behavior per ship.

View File

@@ -19,7 +19,7 @@
class BuildingSystem; class BuildingSystem;
class EntityAdmin; class EntityAdmin;
class ScrapSystem; class DebrisSystem;
struct GameConfig; struct GameConfig;
// Orchestrates ship-behavior decision-making in three batched phases: // Orchestrates ship-behavior decision-making in three batched phases:
@@ -34,7 +34,7 @@ class AiSystem
public: public:
explicit AiSystem(const GameConfig& config); explicit AiSystem(const GameConfig& config);
void tick(EntityAdmin& admin, const BuildingSystem& buildings, const ScrapSystem& scraps); void tick(EntityAdmin& admin, const BuildingSystem& buildings, const DebrisSystem& debris);
private: private:
void selectWinningBehaviors(EntityAdmin& admin); void selectWinningBehaviors(EntityAdmin& admin);

View File

@@ -23,7 +23,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/DebrisSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
PARENT_SCOPE PARENT_SCOPE
) )
@@ -53,7 +53,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DebrisSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -0,0 +1,78 @@
#include "DebrisSystem.h"
#include "DespawnAtComponent.h"
#include "EntityAdmin.h"
#include "PositionComponent.h"
#include "DebrisComponent.h"
#include "tracing.h"
DebrisSystem::DebrisSystem(EntityAdmin& admin)
: m_admin(admin)
{
}
entt::entity DebrisSystem::spawn(QVector2D position, int amount, Tick despawnAt)
{
return m_admin.spawnDebris(position, amount, despawnAt);
}
void DebrisSystem::tickDespawn(Tick currentTick)
{
TRACE();
std::vector<entt::entity> expired;
m_admin.forEach<DespawnAtComponent>(
[&expired, currentTick](entt::entity e, DespawnAtComponent& d)
{
if (d.tick <= currentTick)
{
expired.push_back(e);
}
});
for (entt::entity e : expired)
{
m_admin.destroy(e);
}
}
std::optional<int> DebrisSystem::consume(entt::entity entity)
{
if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
{
return std::nullopt;
}
int amount = m_admin.get<DebrisComponent>(entity).amount;
m_admin.destroy(entity);
return amount;
}
bool DebrisSystem::collectOne(entt::entity entity)
{
if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
{
return false;
}
DebrisComponent& data = m_admin.get<DebrisComponent>(entity);
if (data.amount <= 0)
{
return false;
}
--data.amount;
if (data.amount <= 0)
{
m_admin.destroy(entity);
}
return true;
}
std::vector<DebrisInfo> DebrisSystem::getAllDebrisInfo() const
{
std::vector<DebrisInfo> result;
m_admin.forEach<DebrisComponent>(
[&result, this](entt::entity e, const DebrisComponent& sd)
{
result.push_back(DebrisInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
});
return result;
}

View File

@@ -11,31 +11,35 @@
class EntityAdmin; class EntityAdmin;
struct ScrapInfo // A piece of debris and the scrap amount it still holds (REQ-RES-DEBRIS-DROP).
struct DebrisInfo
{ {
entt::entity entity; entt::entity entity;
QVector2D position; QVector2D position;
int amount; int amount;
}; };
class ScrapSystem // Manages debris entities: the salvageable objects dropped by destroyed ships and
// defence stations (REQ-RES-DEBRIS-DROP). Each piece carries a scrap amount that
// salvage modules collect one unit at a time (REQ-SHP-SALVAGE).
class DebrisSystem
{ {
public: public:
explicit ScrapSystem(EntityAdmin& admin); explicit DebrisSystem(EntityAdmin& admin);
entt::entity spawn(QVector2D position, int amount, Tick despawnAt); entt::entity spawn(QVector2D position, int amount, Tick despawnAt);
void tickDespawn(Tick currentTick); void tickDespawn(Tick currentTick);
// Removes the scrap and returns its amount, or nullopt if not found. // Removes the debris and returns its remaining scrap amount, or nullopt if not found.
std::optional<int> consume(entt::entity entity); std::optional<int> consume(entt::entity entity);
// Collects a single scrap unit from the pile: decrements its amount by one, // Collects a single scrap unit from the debris: decrements its amount by one,
// destroying the entity once depleted. Returns true if a scrap was collected, // destroying the entity once depleted. Returns true if a scrap was collected,
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE). // false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
bool collectOne(entt::entity entity); bool collectOne(entt::entity entity);
// Lightweight snapshot for callers that need to iterate all scrap. // Lightweight snapshot for callers that need to iterate all debris.
std::vector<ScrapInfo> getAllScrapInfo() const; std::vector<DebrisInfo> getAllDebrisInfo() const;
private: private:
EntityAdmin& m_admin; EntityAdmin& m_admin;

View File

@@ -14,8 +14,8 @@
#include "ModuleOwnerComponent.h" #include "ModuleOwnerComponent.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "SalvagerComponent.h" #include "SalvagerComponent.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "tracing.h" #include "tracing.h"
SalvagerSystem::SalvagerSystem(EntityAdmin& admin) SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
@@ -23,14 +23,14 @@ SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
{ {
} }
void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem& buildings, void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
std::vector<BeamFiredEvent>& outBeamFiredEvents) std::vector<BeamFiredEvent>& outBeamFiredEvents)
{ {
TRACE(); TRACE();
// Apply collections whose mid-beam delay has elapsed (cycles started earlier). // Apply collections whose mid-beam delay has elapsed (cycles started earlier).
applyPendingCollections(currentTick, scraps); applyPendingCollections(currentTick, debris);
const std::vector<ScrapInfo> allScrap = scraps.getAllScrapInfo(); const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
// Tick down per-module collection cooldowns. // Tick down per-module collection cooldowns.
m_admin.forEach<SalvagerComponent>( m_admin.forEach<SalvagerComponent>(
@@ -40,8 +40,8 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
}); });
// Scrap units already claimed by not-yet-applied collection cycles, so two // 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 // modules don't both target the last unit of the same debris (the claim would be
// dropped at apply time). A pile is available while its amount exceeds its claims. // dropped at apply time). Debris is available while its amount exceeds its claims.
std::map<entt::entity, int> claimedUnits; std::map<entt::entity, int> claimedUnits;
// Collection cycles already in flight toward each ship's shared cargo pool, so // 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 // concurrent modules on the same ship never start more cycles than the remaining
@@ -49,7 +49,7 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
std::map<entt::entity, int> pendingByShip; std::map<entt::entity, int> pendingByShip;
for (const PendingCollection& pc : m_pendingCollections) for (const PendingCollection& pc : m_pendingCollections)
{ {
++claimedUnits[pc.scrap]; ++claimedUnits[pc.debris];
++pendingByShip[pc.ship]; ++pendingByShip[pc.ship];
} }
@@ -66,12 +66,12 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
if (cargo.current + pendingByShip[o.owner] >= cargo.maxCapacity) { return; } if (cargo.current + pendingByShip[o.owner] >= cargo.maxCapacity) { return; }
const QVector2D ownerPos = m_admin.get<PositionComponent>(o.owner).value; const QVector2D ownerPos = m_admin.get<PositionComponent>(o.owner).value;
for (const ScrapInfo& si : allScrap) for (const DebrisInfo& si : allDebris)
{ {
if ((si.position - ownerPos).length() > s.collectionRange_tiles) { continue; } if ((si.position - ownerPos).length() > s.collectionRange_tiles) { continue; }
if (claimedUnits[si.entity] >= m_admin.get<ScrapDataComponent>(si.entity).amount) if (claimedUnits[si.entity] >= m_admin.get<DebrisComponent>(si.entity).amount)
{ {
continue; // every remaining unit of this pile is already spoken for continue; // every remaining unit of this debris is already spoken for
} }
outBeamFiredEvents.push_back( outBeamFiredEvents.push_back(
BeamFiredEvent{BeamKind::Salvage, o.owner, si.entity, currentTick}); BeamFiredEvent{BeamKind::Salvage, o.owner, si.entity, currentTick});
@@ -107,7 +107,7 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
}); });
} }
void SalvagerSystem::applyPendingCollections(Tick currentTick, ScrapSystem& scraps) void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& debris)
{ {
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin(); std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
while (it != m_pendingCollections.end()) while (it != m_pendingCollections.end())
@@ -117,7 +117,7 @@ void SalvagerSystem::applyPendingCollections(Tick currentTick, ScrapSystem& scra
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship)) if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
{ {
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship); CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
if (cargo.current < cargo.maxCapacity && scraps.collectOne(it->scrap)) if (cargo.current < cargo.maxCapacity && debris.collectOne(it->debris))
{ {
++cargo.current; ++cargo.current;
} }

View File

@@ -9,11 +9,11 @@
class BuildingSystem; class BuildingSystem;
class EntityAdmin; class EntityAdmin;
class ScrapSystem; class DebrisSystem;
// World-mutation system for salvage modules: each module runs a collection cycle // 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 // 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 // in-range piece of debris and schedules the collection of one scrap for mid-beam
// (kBeamImpactDelayTicks later) — mirroring weapon firing. Also delivers full // (kBeamImpactDelayTicks later) — mirroring weapon firing. Also delivers full
// cargo at a SalvageBay. Runs every tick, independent of behavior selection. // cargo at a SalvageBay. Runs every tick, independent of behavior selection.
class SalvagerSystem class SalvagerSystem
@@ -21,18 +21,18 @@ class SalvagerSystem
public: public:
explicit SalvagerSystem(EntityAdmin& admin); explicit SalvagerSystem(EntityAdmin& admin);
void tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem& buildings, void tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
std::vector<BeamFiredEvent>& outBeamFiredEvents); std::vector<BeamFiredEvent>& outBeamFiredEvents);
private: private:
struct PendingCollection struct PendingCollection
{ {
entt::entity ship; entt::entity ship;
entt::entity scrap; entt::entity debris;
Tick appliesAt; Tick appliesAt;
}; };
void applyPendingCollections(Tick currentTick, ScrapSystem& scraps); void applyPendingCollections(Tick currentTick, DebrisSystem& debris);
EntityAdmin& m_admin; EntityAdmin& m_admin;
std::vector<PendingCollection> m_pendingCollections; std::vector<PendingCollection> m_pendingCollections;

View File

@@ -1,78 +0,0 @@
#include "ScrapSystem.h"
#include "DespawnAtComponent.h"
#include "EntityAdmin.h"
#include "PositionComponent.h"
#include "ScrapDataComponent.h"
#include "tracing.h"
ScrapSystem::ScrapSystem(EntityAdmin& admin)
: m_admin(admin)
{
}
entt::entity ScrapSystem::spawn(QVector2D position, int amount, Tick despawnAt)
{
return m_admin.spawnScrap(position, amount, despawnAt);
}
void ScrapSystem::tickDespawn(Tick currentTick)
{
TRACE();
std::vector<entt::entity> expired;
m_admin.forEach<DespawnAtComponent>(
[&expired, currentTick](entt::entity e, DespawnAtComponent& d)
{
if (d.tick <= currentTick)
{
expired.push_back(e);
}
});
for (entt::entity e : expired)
{
m_admin.destroy(e);
}
}
std::optional<int> ScrapSystem::consume(entt::entity entity)
{
if (!m_admin.isValid(entity) || !m_admin.hasAll<ScrapDataComponent>(entity))
{
return std::nullopt;
}
int amount = m_admin.get<ScrapDataComponent>(entity).amount;
m_admin.destroy(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::getAllScrapInfo() const
{
std::vector<ScrapInfo> result;
m_admin.forEach<ScrapDataComponent>(
[&result, this](entt::entity e, const ScrapDataComponent& sd)
{
result.push_back(ScrapInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
});
return result;
}

View File

@@ -96,7 +96,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId,
layout.has_value() ? layout->placedModules : def->defaultModules; layout.has_value() ? layout->placedModules : def->defaultModules;
// Derive the scrap dropped on destruction from the ship's as-built threat cost // Derive the scrap dropped on destruction from the ship's as-built threat cost
// (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat), floored at 1 for any // (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat), floored at 1 for any
// ship with threat > 0. Computed once here since threat is level-independent. // ship with threat > 0. Computed once here since threat is level-independent.
const double threatCost = calculateShipThreatCost(m_config.threatCosts, m_config, const double threatCost = calculateShipThreatCost(m_config.threatCosts, m_config,
schematicId, modules); schematicId, modules);
@@ -392,7 +392,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId,
} }
SalvageScrapBehavior salvage; SalvageScrapBehavior salvage;
salvage.scrapTarget = std::nullopt; salvage.debrisTarget = std::nullopt;
salvage.maxCollectionRange_tiles = maxCollRange; salvage.maxCollectionRange_tiles = maxCollRange;
salvage.orbitRadius_tiles = salvage.orbitRadius_tiles =
maxCollRange * static_cast<float>(m_config.world.orbitFactor); maxCollRange * static_cast<float>(m_config.world.orbitFactor);

View File

@@ -11,15 +11,15 @@
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "SalvageScrapBehavior.h" #include "SalvageScrapBehavior.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "SensorRangeComponent.h" #include "SensorRangeComponent.h"
#include "tracing.h" #include "tracing.h"
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const ScrapSystem& scraps) void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const DebrisSystem& debris)
{ {
TRACE(); TRACE();
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin); const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
const std::vector<ScrapInfo> allScrap = scraps.getAllScrapInfo(); const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>( admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos, [&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,
@@ -31,15 +31,15 @@ void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const ScrapSystem& scra
if (cargoFull) if (cargoFull)
{ {
salvage.scrapTarget = std::nullopt; salvage.debrisTarget = std::nullopt;
salvage.score = BehaviorScores::kInactive; salvage.score = BehaviorScores::kInactive;
return; return;
} }
// Find nearest scrap within sensor range. // Find nearest debris within sensor range.
float bestDist = sensor.value_tiles; float bestDist = sensor.value_tiles;
std::optional<QVector2D> bestPos; std::optional<QVector2D> bestPos;
for (const ScrapInfo& si : allScrap) for (const DebrisInfo& si : allDebris)
{ {
const float dist = (si.position - pos.value).length(); const float dist = (si.position - pos.value).length();
if (dist < bestDist) if (dist < bestDist)
@@ -49,7 +49,7 @@ void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const ScrapSystem& scra
} }
} }
salvage.scrapTarget = bestPos; salvage.debrisTarget = bestPos;
salvage.score = bestPos ? BehaviorScores::kSalvage : BehaviorScores::kInactive; salvage.score = bestPos ? BehaviorScores::kSalvage : BehaviorScores::kInactive;
}); });
} }

View File

@@ -1,13 +1,13 @@
#pragma once #pragma once
class EntityAdmin; class EntityAdmin;
class ScrapSystem; class DebrisSystem;
// When cargo is not full, finds the nearest scrap within sensor range and sets // When cargo is not full, finds the nearest debris within sensor range and sets
// it as the target, scoring high. Scores inactive when cargo is full or no scrap // it as the target, scoring high. Scores inactive when cargo is full or no debris
// is in range (Advance then handles roaming). // is in range (Advance then handles roaming).
class SalvageScrapEvaluator class SalvageScrapEvaluator
{ {
public: public:
void evaluate(EntityAdmin& admin, const ScrapSystem& scraps); void evaluate(EntityAdmin& admin, const DebrisSystem& debris);
}; };

View File

@@ -17,8 +17,8 @@ void SalvageScrapExecutor::execute(EntityAdmin& admin)
MovementIntentComponent& intent) MovementIntentComponent& intent)
{ {
if (selected.winner != BehaviorKind::SalvageScrap) { return; } if (selected.winner != BehaviorKind::SalvageScrap) { return; }
if (!salvage.scrapTarget) { return; } if (!salvage.debrisTarget) { return; }
intent = MovementIntentComponent{true, *salvage.scrapTarget, intent = MovementIntentComponent{true, *salvage.debrisTarget,
salvage.orbitRadius_tiles}; salvage.orbitRadius_tiles};
}); });
} }

View File

@@ -0,0 +1,18 @@
#pragma once
#include <vector>
#include "entt/entity/entity.hpp"
#include "Event.h"
// The set of currently selected debris (REQ-UI-DEBRIS-CLICK-SELECT,
// REQ-UI-DEBRIS-MULTI-SELECT). An empty list means no debris is selected. Debris forms
// its own selection category, mutually exclusive with buildings and entities.
class DebrisSelectionChangedEvent : public Event
{
public:
explicit DebrisSelectionChangedEvent(std::vector<entt::entity> debris)
: debris(std::move(debris)) {}
const std::vector<entt::entity> debris;
};

View File

@@ -1,18 +0,0 @@
#pragma once
#include <vector>
#include "entt/entity/entity.hpp"
#include "Event.h"
// The set of currently selected scrap piles (REQ-UI-SCRAP-CLICK-SELECT,
// REQ-UI-SCRAP-MULTI-SELECT). An empty list means no scrap is selected. Scrap forms
// its own selection category, mutually exclusive with buildings and entities.
class ScrapSelectionChangedEvent : public Event
{
public:
explicit ScrapSelectionChangedEvent(std::vector<entt::entity> scrap)
: scrap(std::move(scrap)) {}
const std::vector<entt::entity> scrap;
};

View File

@@ -5,7 +5,7 @@
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "HealthComponent.h" #include "HealthComponent.h"
@@ -58,16 +58,16 @@ entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
return bestShip; return bestShip;
} }
entt::entity scrapAtWorldPos(EntityAdmin& admin, QVector2D worldPos) entt::entity debrisAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
{ {
// Slightly larger than the scrap's rendered radius (0.2 tiles) so small piles // Slightly larger than the debris's rendered radius (0.2 tiles) so small pieces
// remain easy to click; tunable. // remain easy to click; tunable.
constexpr float kScrapHitRadiusSquared = 0.35f * 0.35f; constexpr float kDebrisHitRadiusSquared = 0.35f * 0.35f;
entt::entity bestScrap = entt::null; entt::entity bestDebris = entt::null;
float bestDistSquared = kScrapHitRadiusSquared; float bestDistSquared = kDebrisHitRadiusSquared;
admin.forEach<ScrapDataComponent, PositionComponent>( admin.forEach<DebrisComponent, PositionComponent>(
[&](entt::entity entity, const ScrapDataComponent& /*sd*/, const PositionComponent& pos) [&](entt::entity entity, const DebrisComponent& /*sd*/, const PositionComponent& pos)
{ {
const float dx = pos.value.x() - worldPos.x(); const float dx = pos.value.x() - worldPos.x();
const float dy = pos.value.y() - worldPos.y(); const float dy = pos.value.y() - worldPos.y();
@@ -75,14 +75,14 @@ entt::entity scrapAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
if (distSquared < bestDistSquared) if (distSquared < bestDistSquared)
{ {
bestDistSquared = distSquared; bestDistSquared = distSquared;
bestScrap = entity; bestDebris = entity;
} }
}); });
return bestScrap; return bestDebris;
} }
std::vector<entt::entity> scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB) std::vector<entt::entity> debrisInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB)
{ {
const int minX = std::min(tileA.x(), tileB.x()); const int minX = std::min(tileA.x(), tileB.x());
const int maxX = std::max(tileA.x(), tileB.x()); const int maxX = std::max(tileA.x(), tileB.x());
@@ -90,8 +90,8 @@ std::vector<entt::entity> scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint ti
const int maxY = std::max(tileA.y(), tileB.y()); const int maxY = std::max(tileA.y(), tileB.y());
std::vector<entt::entity> result; std::vector<entt::entity> result;
admin.forEach<ScrapDataComponent, PositionComponent>( admin.forEach<DebrisComponent, PositionComponent>(
[&](entt::entity entity, const ScrapDataComponent& /*sd*/, const PositionComponent& pos) [&](entt::entity entity, const DebrisComponent& /*sd*/, const PositionComponent& pos)
{ {
const int tileX = static_cast<int>(std::floor(pos.value.x())); const int tileX = static_cast<int>(std::floor(pos.value.x()));
const int tileY = static_cast<int>(std::floor(pos.value.y())); const int tileY = static_cast<int>(std::floor(pos.value.y()));

View File

@@ -11,14 +11,14 @@ class EntityAdmin;
entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos); entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
// Returns the nearest scrap pile whose center is within the scrap pick radius of // Returns the nearest piece of debris whose center is within the debris pick radius of
// worldPos, or entt::null if none (REQ-UI-SCRAP-CLICK-SELECT). Scrap is picked only // worldPos, or entt::null if none (REQ-UI-DEBRIS-CLICK-SELECT). Debris is picked only
// after actors: entityAtWorldPos never returns scrap (scrap has no HealthComponent). // after actors: entityAtWorldPos never returns debris (debris has no HealthComponent).
entt::entity scrapAtWorldPos(EntityAdmin& admin, QVector2D worldPos); entt::entity debrisAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
// Returns every scrap pile whose position falls within the inclusive tile rectangle // Returns every piece of debris whose position falls within the inclusive tile rectangle
// spanned by tileA and tileB, in any corner order (REQ-UI-SCRAP-MULTI-SELECT). // spanned by tileA and tileB, in any corner order (REQ-UI-DEBRIS-MULTI-SELECT).
std::vector<entt::entity> scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB); std::vector<entt::entity> debrisInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB);
// Returns every living actor (ship or defence station, player or enemy) that falls // Returns every living actor (ship or defence station, player or enemy) that falls
// within the inclusive tile rectangle spanned by tileA and tileB, in any corner order // within the inclusive tile rectangle spanned by tileA and tileB, in any corner order

View File

@@ -20,8 +20,8 @@
#include "PositionComponent.h" #include "PositionComponent.h"
#include "RepairSystem.h" #include "RepairSystem.h"
#include "SalvagerSystem.h" #include "SalvagerSystem.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "StateChecksum.h" #include "StateChecksum.h"
@@ -69,7 +69,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
m_aiSystem = std::make_unique<AiSystem>(m_config); m_aiSystem = std::make_unique<AiSystem>(m_config);
m_movementIntentSystem = std::make_unique<MovementIntentSystem>(); m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>(); m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin); m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin); m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
m_repairSystem = std::make_unique<RepairSystem>(m_admin); m_repairSystem = std::make_unique<RepairSystem>(m_admin);
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng); m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
@@ -141,7 +141,7 @@ void Simulation::reset(unsigned int seed)
m_aiSystem = std::make_unique<AiSystem>(m_config); m_aiSystem = std::make_unique<AiSystem>(m_config);
m_movementIntentSystem = std::make_unique<MovementIntentSystem>(); m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>(); m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin); m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin); m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
m_repairSystem = std::make_unique<RepairSystem>(m_admin); m_repairSystem = std::make_unique<RepairSystem>(m_admin);
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng); m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
@@ -329,10 +329,10 @@ void Simulation::tick()
m_shipSystem->clearMovementIntents(); m_shipSystem->clearMovementIntents();
// Score-based behavior selection: evaluate, select winner, execute (sets // Score-based behavior selection: evaluate, select winner, execute (sets
// movement intent + preferred module targets only — no world mutation). // movement intent + preferred module targets only — no world mutation).
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_scrapSystem); m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
// Module systems perform the world mutation (collection/delivery, healing). // Module systems perform the world mutation (collection/delivery, healing).
// Each emits its tool beams and applies its own delayed (mid-beam) effects. // Each emits its tool beams and applies its own delayed (mid-beam) effects.
m_salvagerSystem->tick(m_currentTick, *m_scrapSystem, *m_buildingSystem, m_beamFiredEvents); m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, m_beamFiredEvents);
m_repairSystem->tick(m_currentTick, m_beamFiredEvents); m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
// Step 8: combat resolution // Step 8: combat resolution
@@ -352,8 +352,8 @@ void Simulation::tick()
m_movementIntentSystem->tick(m_admin); m_movementIntentSystem->tick(m_admin);
m_dynamicBodySystem->tick(m_admin); m_dynamicBodySystem->tick(m_admin);
// Step 11: scrap despawn // Step 11: debris despawn
m_scrapSystem->tickDespawn(m_currentTick); m_debrisSystem->tickDespawn(m_currentTick);
++m_currentTick; ++m_currentTick;
} }
@@ -542,8 +542,8 @@ void Simulation::tickDeathsAndLoot()
if (si.scrapDrop > 0) if (si.scrapDrop > 0)
{ {
const Tick despawnAt = m_currentTick const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds); + secondsToTicks(m_config.world.debrisDespawnSeconds);
m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt); m_debrisSystem->spawn(pos.value, si.scrapDrop, despawnAt);
} }
m_shipSystem->despawn(deadEntity); m_shipSystem->despawn(deadEntity);
} }
@@ -567,7 +567,7 @@ void Simulation::tickDeathsAndLoot()
const FactionComponent& fac = m_admin.get<FactionComponent>(deadEntity); const FactionComponent& fac = m_admin.get<FactionComponent>(deadEntity);
const Tick despawnAt = m_currentTick const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds); + secondsToTicks(m_config.world.debrisDespawnSeconds);
int scrap = 0; int scrap = 0;
if (!fac.isEnemy) if (!fac.isEnemy)
{ {
@@ -584,7 +584,7 @@ void Simulation::tickDeathsAndLoot()
} }
if (scrap > 0) if (scrap > 0)
{ {
m_scrapSystem->spawn(pos.value, scrap, despawnAt); m_debrisSystem->spawn(pos.value, scrap, despawnAt);
} }
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells); m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
{ {
@@ -1000,8 +1000,8 @@ unsigned long long Simulation::computeStateChecksum() const
hasher.append(c.linearAcceleration_tptt); hasher.append(c.linearAcceleration_tptt);
hasher.append(c.angularAcceleration_rptt); hasher.append(c.angularAcceleration_rptt);
}); });
m_admin.forEach<ScrapDataComponent>( m_admin.forEach<DebrisComponent>(
[&hasher](entt::entity entity, const ScrapDataComponent& c) [&hasher](entt::entity entity, const DebrisComponent& c)
{ {
hasher.append(static_cast<std::uint32_t>(entity)); hasher.append(static_cast<std::uint32_t>(entity));
hasher.append(c.amount); hasher.append(c.amount);
@@ -1238,14 +1238,14 @@ const ShipSystem& Simulation::getShips() const
return *m_shipSystem; return *m_shipSystem;
} }
ScrapSystem& Simulation::getScraps() DebrisSystem& Simulation::getDebrisSystem()
{ {
return *m_scrapSystem; return *m_debrisSystem;
} }
const ScrapSystem& Simulation::getScraps() const const DebrisSystem& Simulation::getDebrisSystem() const
{ {
return *m_scrapSystem; return *m_debrisSystem;
} }
EntityAdmin& Simulation::getAdmin() EntityAdmin& Simulation::getAdmin()

View File

@@ -33,7 +33,7 @@ class MovementIntentSystem;
class RepairSystem; class RepairSystem;
class SalvagerSystem; class SalvagerSystem;
class ShipSystem; class ShipSystem;
class ScrapSystem; class DebrisSystem;
class WaveSystem; class WaveSystem;
class Simulation: public CombinedEventHandler<TracePrintRequestedEvent> class Simulation: public CombinedEventHandler<TracePrintRequestedEvent>
@@ -122,8 +122,8 @@ public:
const BeltSystem& getBelts() const; const BeltSystem& getBelts() const;
ShipSystem& getShips(); ShipSystem& getShips();
const ShipSystem& getShips() const; const ShipSystem& getShips() const;
ScrapSystem& getScraps(); DebrisSystem& getDebrisSystem();
const ScrapSystem& getScraps() const; const DebrisSystem& getDebrisSystem() const;
EntityAdmin& getAdmin(); EntityAdmin& getAdmin();
const EntityAdmin& getAdmin() const; const EntityAdmin& getAdmin() const;
@@ -172,7 +172,7 @@ private:
// Stores their IDs in m_currentEnemyStationIds. // Stores their IDs in m_currentEnemyStationIds.
void placeEnemyStationSet(int generation); void placeEnemyStationSet(int generation);
// Tick step 9: remove dead ships and buildings, drop scrap, handle push. // Tick step 9: remove dead ships and buildings, drop debris, handle push.
void tickDeathsAndLoot(); void tickDeathsAndLoot();
// Generate up to 3 schematic choices (REQ-DEF-SCHEMATIC-DROP) for the player. // Generate up to 3 schematic choices (REQ-DEF-SCHEMATIC-DROP) for the player.
@@ -269,7 +269,7 @@ private:
std::unique_ptr<AiSystem> m_aiSystem; std::unique_ptr<AiSystem> m_aiSystem;
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem; std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem; std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
std::unique_ptr<ScrapSystem> m_scrapSystem; std::unique_ptr<DebrisSystem> m_debrisSystem;
std::unique_ptr<SalvagerSystem> m_salvagerSystem; std::unique_ptr<SalvagerSystem> m_salvagerSystem;
std::unique_ptr<RepairSystem> m_repairSystem; std::unique_ptr<RepairSystem> m_repairSystem;
std::unique_ptr<WaveSystem> m_waveSystem; std::unique_ptr<WaveSystem> m_waveSystem;

View File

@@ -66,7 +66,7 @@ ThreatCostTable computeThreatCostTable(const GameConfig& config)
ThreatCostTable table; ThreatCostTable table;
// Scrap threat (REQ-THREAT-SCRAP) is the constant inverse of the scrap-drop // Scrap threat (REQ-THREAT-SCRAP) is the constant inverse of the scrap-drop
// conversion (REQ-RES-SCRAP-DROP): one scrap is worth 1 / scrap_per_threat. // conversion (REQ-RES-DEBRIS-DROP): one scrap is worth 1 / scrap_per_threat.
// Set it up front so reprocessing-only item threats (below) can use it, and so // Set it up front so reprocessing-only item threats (below) can use it, and so
// it no longer depends on any ship's threat cost. // it no longer depends on any ship's threat cost.
table.scrapThreat = config.world.scrapPerThreat > 0.0 table.scrapThreat = config.world.scrapPerThreat > 0.0

View File

@@ -38,7 +38,7 @@
#include "SalvageScrapBehavior.h" #include "SalvageScrapBehavior.h"
#include "SalvagerComponent.h" #include "SalvagerComponent.h"
#include "SalvagerSystem.h" #include "SalvagerSystem.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "SelectedBehaviorComponent.h" #include "SelectedBehaviorComponent.h"
#include "SensorRangeComponent.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
@@ -70,7 +70,7 @@ struct Fixture
RepairSystem repair; RepairSystem repair;
MovementIntentSystem movementIntent; MovementIntentSystem movementIntent;
DynamicBodySystem dynamicBody; DynamicBodySystem dynamicBody;
ScrapSystem scraps; DebrisSystem scraps;
Tick tick; Tick tick;
std::vector<BeamFiredEvent> beamEvents; std::vector<BeamFiredEvent> beamEvents;
@@ -1288,7 +1288,7 @@ TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[senso
f.decide(); f.decide();
REQUIRE_FALSE(f.admin.get<SalvageScrapBehavior>(ship).scrapTarget.has_value()); REQUIRE_FALSE(f.admin.get<SalvageScrapBehavior>(ship).debrisTarget.has_value());
REQUIRE(intent(f.admin, ship).target.x() > pos(f.admin, ship).value.x()); REQUIRE(intent(f.admin, ship).target.x() > pos(f.admin, ship).value.x());
} }

View File

@@ -13,7 +13,7 @@ add_files(
BuildingTest.cpp BuildingTest.cpp
BuildingConfigTest.cpp BuildingConfigTest.cpp
ShipTest.cpp ShipTest.cpp
ScrapTest.cpp DebrisTest.cpp
BehaviorSystemTest.cpp BehaviorSystemTest.cpp
WaveSystemTest.cpp WaveSystemTest.cpp
CombatSystemTest.cpp CombatSystemTest.cpp

View File

@@ -14,8 +14,8 @@
#include "HealthComponent.h" #include "HealthComponent.h"
#include "HqProxyComponent.h" #include "HqProxyComponent.h"
#include "ModuleOwnerComponent.h" #include "ModuleOwnerComponent.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Simulation.h" #include "Simulation.h"
#include "AttackBehavior.h" #include "AttackBehavior.h"
@@ -408,7 +408,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
// Scrap dropped on death is derived from the ship's as-built threat cost // Scrap dropped on death is derived from the ship's as-built threat cost
// (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat). The interceptor's // (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat). The interceptor's
// threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops // threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops
// round(59.0 * 1.0) = 59 scrap. // round(59.0 * 1.0) = 59 scrap.
const entt::entity ship = sim.getShips().spawn("interceptor", const entt::entity ship = sim.getShips().spawn("interceptor",
@@ -417,9 +417,9 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
sim.tick(); sim.tick();
const std::vector<ScrapInfo> scraps = sim.getScraps().getAllScrapInfo(); const std::vector<DebrisInfo> scraps = sim.getDebrisSystem().getAllDebrisInfo();
REQUIRE(scraps.size() == 1); REQUIRE(scraps.size() == 1);
CHECK(sim.getAdmin().get<ScrapDataComponent>(scraps[0].entity).amount == 59); CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
} }
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]") TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")

View File

@@ -212,7 +212,7 @@ TEST_CASE("Missing field in world.toml is rejected with the field path", "[confi
height_tiles = 60 height_tiles = 60
refund_percentage = 75 refund_percentage = 75
deconstruction_time_seconds = 0.1 deconstruction_time_seconds = 0.1
scrap_despawn_seconds = 30 debris_despawn_seconds = 30
scrap_per_threat = 0.01 scrap_per_threat = 0.01
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20
@@ -264,7 +264,7 @@ TEST_CASE("Malformed formula in world.toml is rejected with field identification
height_tiles = 60 height_tiles = 60
refund_percentage = 75 refund_percentage = 75
deconstruction_time_seconds = 0.1 deconstruction_time_seconds = 0.1
scrap_despawn_seconds = 30 debris_despawn_seconds = 30
scrap_per_threat = 0.01 scrap_per_threat = 0.01
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20
@@ -317,7 +317,7 @@ TEST_CASE("Inverted wave gap range is rejected", "[config]")
height_tiles = 60 height_tiles = 60
refund_percentage = 75 refund_percentage = 75
deconstruction_time_seconds = 0.1 deconstruction_time_seconds = 0.1
scrap_despawn_seconds = 30 debris_despawn_seconds = 30
scrap_per_threat = 0.01 scrap_per_threat = 0.01
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20

View File

@@ -8,8 +8,8 @@
#include "DespawnAtComponent.h" #include "DespawnAtComponent.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "EntityHitTest.h" #include "EntityHitTest.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
namespace namespace
{ {
@@ -23,15 +23,15 @@ bool contains(const std::vector<entt::entity>& v, entt::entity e)
// Spawn // Spawn
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("ScrapSystem: spawn returns a valid entity with correct scrap data", "[scrap]") TEST_CASE("DebrisSystem: spawn returns a valid entity with correct debris data", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
REQUIRE(admin.get<ScrapDataComponent>(e).amount == 5); REQUIRE(admin.get<DebrisComponent>(e).amount == 5);
REQUIRE(admin.get<DespawnAtComponent>(e).tick == 100); REQUIRE(admin.get<DespawnAtComponent>(e).tick == 100);
} }
@@ -39,10 +39,10 @@ TEST_CASE("ScrapSystem: spawn returns a valid entity with correct scrap data", "
// Despawn timing // Despawn timing
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("ScrapSystem: scrap still present one tick before despawnAt", "[scrap]") TEST_CASE("DebrisSystem: debris still present one tick before despawnAt", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50);
@@ -50,10 +50,10 @@ TEST_CASE("ScrapSystem: scrap still present one tick before despawnAt", "[scrap]
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
} }
TEST_CASE("ScrapSystem: scrap removed at despawnAt tick", "[scrap]") TEST_CASE("DebrisSystem: debris removed at despawnAt tick", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50);
@@ -65,10 +65,10 @@ TEST_CASE("ScrapSystem: scrap removed at despawnAt tick", "[scrap]")
// Selective removal // Selective removal
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("ScrapSystem: tickDespawn removes only expired scraps", "[scrap]") TEST_CASE("DebrisSystem: tickDespawn removes only expired debris", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity earlyE = ss.spawn(QVector2D(0.0f, 0.0f), 1, 30); const entt::entity earlyE = ss.spawn(QVector2D(0.0f, 0.0f), 1, 30);
const entt::entity lateE = ss.spawn(QVector2D(1.0f, 0.0f), 2, 60); const entt::entity lateE = ss.spawn(QVector2D(1.0f, 0.0f), 2, 60);
@@ -83,10 +83,10 @@ TEST_CASE("ScrapSystem: tickDespawn removes only expired scraps", "[scrap]")
// Consume // Consume
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("ScrapSystem: consume returns amount and destroys entity", "[scrap]") TEST_CASE("DebrisSystem: consume returns amount and destroys entity", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 7, 100); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 7, 100);
@@ -96,10 +96,10 @@ TEST_CASE("ScrapSystem: consume returns amount and destroys entity", "[scrap]")
REQUIRE_FALSE(admin.isValid(e)); REQUIRE_FALSE(admin.isValid(e));
} }
TEST_CASE("ScrapSystem: consume returns nullopt for invalid entity", "[scrap]") TEST_CASE("DebrisSystem: consume returns nullopt for invalid entity", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const std::optional<int> amount = ss.consume(entt::null); const std::optional<int> amount = ss.consume(entt::null);
REQUIRE_FALSE(amount.has_value()); REQUIRE_FALSE(amount.has_value());
@@ -109,118 +109,118 @@ TEST_CASE("ScrapSystem: consume returns nullopt for invalid entity", "[scrap]")
// collectOne // collectOne
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("ScrapSystem: collectOne depletes one scrap and keeps the pile until empty", "[scrap]") TEST_CASE("DebrisSystem: collectOne depletes one scrap and keeps the debris until empty", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
REQUIRE(ss.collectOne(e)); REQUIRE(ss.collectOne(e));
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
REQUIRE(admin.get<ScrapDataComponent>(e).amount == 2); REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
REQUIRE(ss.collectOne(e)); REQUIRE(ss.collectOne(e));
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
REQUIRE(admin.get<ScrapDataComponent>(e).amount == 1); REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
// Final unit collected: the pile is removed once depleted. // Final unit collected: the debris is removed once depleted.
REQUIRE(ss.collectOne(e)); REQUIRE(ss.collectOne(e));
REQUIRE_FALSE(admin.isValid(e)); REQUIRE_FALSE(admin.isValid(e));
} }
TEST_CASE("ScrapSystem: collectOne returns false for an invalid entity", "[scrap]") TEST_CASE("DebrisSystem: collectOne returns false for an invalid entity", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
REQUIRE_FALSE(ss.collectOne(entt::null)); REQUIRE_FALSE(ss.collectOne(entt::null));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// allScrapInfo // getAllDebrisInfo
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("ScrapSystem: allScrapInfo returns all spawned scrap", "[scrap]") TEST_CASE("DebrisSystem: getAllDebrisInfo returns all spawned debris", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
ss.spawn(QVector2D(1.0f, 2.0f), 3, 100); ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200); ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
const std::vector<ScrapInfo> info = ss.getAllScrapInfo(); const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
REQUIRE(info.size() == 2); REQUIRE(info.size() == 2);
} }
TEST_CASE("ScrapSystem: allScrapInfo reports each pile's remaining amount", "[scrap]") TEST_CASE("DebrisSystem: getAllDebrisInfo reports each debris entry.s remaining amount", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100); const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200); const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
const std::vector<ScrapInfo> info = ss.getAllScrapInfo(); const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
REQUIRE(info.size() == 2); REQUIRE(info.size() == 2);
for (const ScrapInfo& i : info) for (const DebrisInfo& i : info)
{ {
if (i.entity == a) { REQUIRE(i.amount == 3); } if (i.entity == a) { REQUIRE(i.amount == 3); }
else if (i.entity == b) { REQUIRE(i.amount == 6); } else if (i.entity == b) { REQUIRE(i.amount == 6); }
else { FAIL("unexpected scrap entity"); } else { FAIL("unexpected debris entity"); }
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Selection hit-testing (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT) // Selection hit-testing (REQ-UI-DEBRIS-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("scrapAtWorldPos returns the pile near a point and null when far", "[scrap]") TEST_CASE("debrisAtWorldPos returns the debris near a point and null when far", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);
// Extra parens keep Catch from decomposing the comparison, which is ambiguous // Extra parens keep Catch from decomposing the comparison, which is ambiguous
// between Catch's expression templates and entt's entity operator==. // between Catch's expression templates and entt's entity operator==.
REQUIRE((scrapAtWorldPos(admin, QVector2D(3.1f, 4.0f)) == e)); REQUIRE((debrisAtWorldPos(admin, QVector2D(3.1f, 4.0f)) == e));
REQUIRE((scrapAtWorldPos(admin, QVector2D(10.0f, 10.0f)) == entt::null)); REQUIRE((debrisAtWorldPos(admin, QVector2D(10.0f, 10.0f)) == entt::null));
} }
TEST_CASE("scrapAtWorldPos returns the nearest of several piles", "[scrap]") TEST_CASE("debrisAtWorldPos returns the nearest of several debris", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity near = ss.spawn(QVector2D(2.0f, 2.0f), 1, 100); const entt::entity near = ss.spawn(QVector2D(2.0f, 2.0f), 1, 100);
ss.spawn(QVector2D(2.4f, 2.0f), 1, 100); ss.spawn(QVector2D(2.4f, 2.0f), 1, 100);
REQUIRE((scrapAtWorldPos(admin, QVector2D(2.05f, 2.0f)) == near)); REQUIRE((debrisAtWorldPos(admin, QVector2D(2.05f, 2.0f)) == near));
} }
TEST_CASE("entityAtWorldPos never returns a scrap pile", "[scrap]") TEST_CASE("entityAtWorldPos never returns debris", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);
// Scrap has no HealthComponent, so the actor hit-test ignores it entirely. // Debris has no HealthComponent, so the actor hit-test ignores it entirely.
REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null)); REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null));
} }
TEST_CASE("scrapInBox returns exactly the piles inside the tile rectangle", "[scrap]") TEST_CASE("debrisInBox returns exactly the debris inside the tile rectangle", "[debris]")
{ {
EntityAdmin admin; EntityAdmin admin;
ScrapSystem ss(admin); DebrisSystem ss(admin);
const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100); // tile (1,2) const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100); // tile (1,2)
const entt::entity inB = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100); // tile (4,5) const entt::entity inB = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100); // tile (4,5)
const entt::entity outX = ss.spawn(QVector2D(10.0f, 10.0f), 1, 100); const entt::entity outX = ss.spawn(QVector2D(10.0f, 10.0f), 1, 100);
// Box given in reversed corner order to confirm normalization. // Box given in reversed corner order to confirm normalization.
const std::vector<entt::entity> hit = scrapInBox(admin, QPoint(5, 5), QPoint(0, 0)); const std::vector<entt::entity> hit = debrisInBox(admin, QPoint(5, 5), QPoint(0, 0));
REQUIRE(hit.size() == 2); REQUIRE(hit.size() == 2);
REQUIRE(contains(hit, inA)); REQUIRE(contains(hit, inA));
@@ -228,7 +228,7 @@ TEST_CASE("scrapInBox returns exactly the piles inside the tile rectangle", "[sc
REQUIRE_FALSE(contains(hit, outX)); REQUIRE_FALSE(contains(hit, outX));
} }
TEST_CASE("actorsInBox returns living ships and stations, excluding scrap and dead actors", TEST_CASE("actorsInBox returns living ships and stations, excluding debris and dead actors",
"[actor]") "[actor]")
{ {
EntityAdmin admin; EntityAdmin admin;
@@ -256,8 +256,8 @@ TEST_CASE("actorsInBox returns living ships and stations, excluding scrap and de
const entt::entity station = admin.spawnStation( const entt::entity station = admin.spawnStation(
QPoint(2, 2), QSize(2, 1), stationCells, 200.0f, 200.0f, true); QPoint(2, 2), QSize(2, 1), stationCells, 200.0f, 200.0f, true);
// Scrap and the HQ proxy are never actors. // Debris and the HQ proxy are never actors.
admin.spawnScrap(QVector2D(1.0f, 1.0f), 5, Tick(1000)); admin.spawnDebris(QVector2D(1.0f, 1.0f), 5, Tick(1000));
admin.spawnHqProxy(QVector2D(0.5f, 0.5f), 500.0f, 500.0f); admin.spawnHqProxy(QVector2D(0.5f, 0.5f), 500.0f, 500.0f);
const std::vector<entt::entity> hit = actorsInBox(admin, QPoint(5, 5), QPoint(0, 0)); const std::vector<entt::entity> hit = actorsInBox(admin, QPoint(5, 5), QPoint(0, 0));

View File

@@ -225,7 +225,7 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
REQUIRE(admin.get<CargoComponent>(e).maxCapacity == 10); REQUIRE(admin.get<CargoComponent>(e).maxCapacity == 10);
REQUIRE(admin.get<CargoComponent>(e).current == 0); REQUIRE(admin.get<CargoComponent>(e).current == 0);
REQUIRE_FALSE(admin.get<DeliverScrapBehavior>(e).deliveryBay.has_value()); REQUIRE_FALSE(admin.get<DeliverScrapBehavior>(e).deliveryBay.has_value());
REQUIRE_FALSE(admin.get<SalvageScrapBehavior>(e).scrapTarget.has_value()); REQUIRE_FALSE(admin.get<SalvageScrapBehavior>(e).debrisTarget.has_value());
REQUIRE(admin.get<SalvageScrapBehavior>(e).maxCollectionRange_tiles == Approx(50.0f)); REQUIRE(admin.get<SalvageScrapBehavior>(e).maxCollectionRange_tiles == Approx(50.0f));
} }

View File

@@ -52,15 +52,15 @@
#include "PositionComponent.h" #include "PositionComponent.h"
#include "RepairBehavior.h" #include "RepairBehavior.h"
#include "SalvageScrapBehavior.h" #include "SalvageScrapBehavior.h"
#include "ScrapSelectionChangedEvent.h" #include "DebrisSelectionChangedEvent.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "SelectionChangedEvent.h" #include "SelectionChangedEvent.h"
#include "SensorRangeComponent.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Simulation.h" #include "Simulation.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "ScrapDataComponent.h" #include "DebrisComponent.h"
#include "SurfaceMask.h" #include "SurfaceMask.h"
#include "Tick.h" #include "Tick.h"
#include "TunnelCompletion.h" #include "TunnelCompletion.h"
@@ -350,9 +350,9 @@ void GameWorldView::onFrame()
m_activeBeams = std::move(live); m_activeBeams = std::move(live);
} }
// Drop selected scrap piles that were collected or despawned this frame, so the // Drop selected debris that were collected or despawned this frame, so the
// panel stops counting them and the selection empties out (REQ-UI-SCRAP-CLICK-SELECT). // panel stops counting them and the selection empties out (REQ-UI-DEBRIS-CLICK-SELECT).
pruneDespawnedScrap(); pruneDespawnedDebris();
pruneDespawnedActors(); pruneDespawnedActors();
// Expire copy/paste flashes. Lifetime is wall-clock (the frame delta), so the // Expire copy/paste flashes. Lifetime is wall-clock (the frame delta), so the
@@ -504,7 +504,7 @@ void GameWorldView::paintGL()
drawCopyConfigFeedback(painter); drawCopyConfigFeedback(painter);
drawStations(painter); drawStations(painter);
drawBeltItems(painter); drawBeltItems(painter);
drawScrap(painter); drawDebris(painter);
if (m_debugDraw) if (m_debugDraw)
{ {
drawDebugSensorRanges(painter); drawDebugSensorRanges(painter);
@@ -779,33 +779,33 @@ std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) cons
return m_sim->getAdmin().get<PositionComponent>(entity).value; return m_sim->getAdmin().get<PositionComponent>(entity).value;
} }
void GameWorldView::clearScrapSelection() void GameWorldView::clearDebrisSelection()
{ {
if (m_selectedScrap.empty()) { return; } if (m_selectedDebris.empty()) { return; }
m_selectedScrap.clear(); m_selectedDebris.clear();
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap)); std::make_shared<DebrisSelectionChangedEvent>(m_selectedDebris));
} }
void GameWorldView::pruneDespawnedScrap() void GameWorldView::pruneDespawnedDebris()
{ {
if (m_selectedScrap.empty()) { return; } if (m_selectedDebris.empty()) { return; }
std::vector<entt::entity> live; std::vector<entt::entity> live;
for (const ScrapInfo& info : m_sim->getScraps().getAllScrapInfo()) for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity) if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
!= m_selectedScrap.end()) != m_selectedDebris.end())
{ {
live.push_back(info.entity); live.push_back(info.entity);
} }
} }
if (live.size() != m_selectedScrap.size()) if (live.size() != m_selectedDebris.size())
{ {
m_selectedScrap = std::move(live); m_selectedDebris = std::move(live);
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap)); std::make_shared<DebrisSelectionChangedEvent>(m_selectedDebris));
} }
} }
@@ -1510,16 +1510,16 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
painter.drawRect(rect->adjusted(-1, -1, 1, 1)); painter.drawRect(rect->adjusted(-1, -1, 1, 1));
} }
// A ring around each selected scrap pile, sitting just outside the pile's // A ring around each selected piece of debris, sitting just outside the debris's
// rendered circle (radius getTilePx()*0.2, matching drawScrap) (REQ-UI-SCRAP-CLICK-SELECT). // rendered circle (radius getTilePx()*0.2, matching drawDebris) (REQ-UI-DEBRIS-CLICK-SELECT).
if (!m_selectedScrap.empty()) if (!m_selectedDebris.empty())
{ {
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0; const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo()) for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), scrap.entity) if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity)
== m_selectedScrap.end()) { continue; } == m_selectedDebris.end()) { continue; }
painter.drawEllipse(worldToWidget(scrap.position), outlineRadius, outlineRadius); painter.drawEllipse(worldToWidget(debris.position), outlineRadius, outlineRadius);
} }
} }
} }
@@ -1646,12 +1646,12 @@ void GameWorldView::drawBeltItems(QPainter& painter)
}); });
} }
void GameWorldView::drawScrap(QPainter& painter) void GameWorldView::drawDebris(QPainter& painter)
{ {
const float r = getTilePx() * 0.2f; const float r = getTilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo()) for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
const QPointF center = worldToWidget(scrap.position); const QPointF center = worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90)); painter.setBrush(QColor(128, 110, 90));
painter.setPen(QPen(QColor(50, 40, 30), 1)); painter.setPen(QPen(QColor(50, 40, 30), 1));
painter.drawEllipse(center, painter.drawEllipse(center,
@@ -1838,9 +1838,9 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const SalvageScrapBehavior& salvage) const PositionComponent& pos, const SalvageScrapBehavior& salvage)
{ {
if (!salvage.scrapTarget.has_value()) { return; } if (!salvage.debrisTarget.has_value()) { return; }
drawTargetLine(si.schematicId, pos.value, *salvage.scrapTarget); drawTargetLine(si.schematicId, pos.value, *salvage.debrisTarget);
}); });
} }
@@ -2567,7 +2567,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0; const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
const QVector2D worldPos = widgetToWorld(event->pos()); const QVector2D worldPos = widgetToWorld(event->pos());
// Point hit-test precedence: buildings win over actors, which win over scrap // Point hit-test precedence: buildings win over actors, which win over debris
// (REQ-UI-SELECTION-CATEGORIES). // (REQ-UI-SELECTION-CATEGORIES).
std::optional<BuildingId> buildingHit = buildingAtTile(tile); std::optional<BuildingId> buildingHit = buildingAtTile(tile);
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); } if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
@@ -2576,9 +2576,9 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
{ {
const BuildingId id = *buildingHit; const BuildingId id = *buildingHit;
// A building selection is exclusive: it clears any field selection — // A building selection is exclusive: it clears any field selection —
// actors and scrap — because buildings win (REQ-UI-SELECTION-CATEGORIES). // actors and debris — because buildings win (REQ-UI-SELECTION-CATEGORIES).
clearEntitySelection(); clearEntitySelection();
clearScrapSelection(); clearDebrisSelection();
if (ctrl) if (ctrl)
{ {
bool found = false; bool found = false;
@@ -2600,8 +2600,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
return; return;
} }
// Selecting a field object (actor or scrap) clears any building selection but // Selecting a field object (actor or debris) clears any building selection but
// lets actors and scrap coexist (REQ-UI-SELECTION-CATEGORIES). // lets actors and debris coexist (REQ-UI-SELECTION-CATEGORIES).
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos); const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
if (actorHit != entt::null) if (actorHit != entt::null)
{ {
@@ -2613,7 +2613,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
} }
if (ctrl) if (ctrl)
{ {
// Toggle this actor within the field selection, leaving scrap intact // Toggle this actor within the field selection, leaving debris intact
// (REQ-UI-ENTITY-CLICK-SELECT). // (REQ-UI-ENTITY-CLICK-SELECT).
bool found = false; bool found = false;
std::vector<entt::entity> newSel; std::vector<entt::entity> newSel;
@@ -2629,15 +2629,15 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
{ {
// A plain click makes this actor the sole selection. // A plain click makes this actor the sole selection.
m_selectedEntities = { actorHit }; m_selectedEntities = { actorHit };
clearScrapSelection(); clearDebrisSelection();
} }
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities)); std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities));
return; return;
} }
if (const entt::entity scrapHit = if (const entt::entity debrisHit =
scrapAtWorldPos(m_sim->getAdmin(), worldPos); scrapHit != entt::null) debrisAtWorldPos(m_sim->getAdmin(), worldPos); debrisHit != entt::null)
{ {
if (!m_selectedBuildingIds.empty()) if (!m_selectedBuildingIds.empty())
{ {
@@ -2647,26 +2647,26 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
} }
if (ctrl) if (ctrl)
{ {
// Toggle this pile within the field selection, leaving actors intact // Toggle this debris within the field selection, leaving actors intact
// (REQ-UI-SCRAP-MULTI-SELECT). // (REQ-UI-DEBRIS-MULTI-SELECT).
bool found = false; bool found = false;
std::vector<entt::entity> newSel; std::vector<entt::entity> newSel;
for (entt::entity sel : m_selectedScrap) for (entt::entity sel : m_selectedDebris)
{ {
if (sel == scrapHit) { found = true; } if (sel == debrisHit) { found = true; }
else { newSel.push_back(sel); } else { newSel.push_back(sel); }
} }
if (!found) { newSel.push_back(scrapHit); } if (!found) { newSel.push_back(debrisHit); }
m_selectedScrap = newSel; m_selectedDebris = newSel;
} }
else else
{ {
// A plain click makes this pile the sole selection. // A plain click makes this debris the sole selection.
m_selectedScrap = { scrapHit }; m_selectedDebris = { debrisHit };
clearEntitySelection(); clearEntitySelection();
} }
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap)); std::make_shared<DebrisSelectionChangedEvent>(m_selectedDebris));
return; return;
} }
@@ -2681,7 +2681,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds)); std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
} }
clearEntitySelection(); clearEntitySelection();
clearScrapSelection(); clearDebrisSelection();
} }
m_boxSelecting = true; m_boxSelecting = true;
m_boxStartTile = tile; m_boxStartTile = tile;
@@ -2815,9 +2815,9 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
if (!boxIds.empty()) if (!boxIds.empty())
{ {
// A box covering any building selects buildings; field objects (actors and // A box covering any building selects buildings; field objects (actors and
// scrap) in the box are ignored — buildings win (REQ-UI-MULTI-SELECT). // debris) in the box are ignored — buildings win (REQ-UI-MULTI-SELECT).
clearEntitySelection(); clearEntitySelection();
clearScrapSelection(); clearDebrisSelection();
if (!ctrl) if (!ctrl)
{ {
m_selectedBuildingIds = boxIds; m_selectedBuildingIds = boxIds;
@@ -2840,12 +2840,12 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
} }
// No buildings in the box: select the field objects it covers — ships, defence // No buildings in the box: select the field objects it covers — ships, defence
// stations, and scrap together (REQ-UI-MULTI-SELECT, REQ-UI-SCRAP-MULTI-SELECT). // stations, and debris together (REQ-UI-MULTI-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
const std::vector<entt::entity> boxActors = const std::vector<entt::entity> boxActors =
actorsInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile); actorsInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
const std::vector<entt::entity> boxScrap = const std::vector<entt::entity> boxDebris =
scrapInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile); debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
if (!boxActors.empty() || !boxScrap.empty()) if (!boxActors.empty() || !boxDebris.empty())
{ {
if (!m_selectedBuildingIds.empty()) if (!m_selectedBuildingIds.empty())
{ {
@@ -2856,7 +2856,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
if (!ctrl) if (!ctrl)
{ {
m_selectedEntities = boxActors; m_selectedEntities = boxActors;
m_selectedScrap = boxScrap; m_selectedDebris = boxDebris;
} }
else else
{ {
@@ -2869,20 +2869,20 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
} }
if (!found) { m_selectedEntities.push_back(e); } if (!found) { m_selectedEntities.push_back(e); }
} }
for (entt::entity e : boxScrap) for (entt::entity e : boxDebris)
{ {
bool found = false; bool found = false;
for (entt::entity sel : m_selectedScrap) for (entt::entity sel : m_selectedDebris)
{ {
if (sel == e) { found = true; break; } if (sel == e) { found = true; break; }
} }
if (!found) { m_selectedScrap.push_back(e); } if (!found) { m_selectedDebris.push_back(e); }
} }
} }
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities)); std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities));
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap)); std::make_shared<DebrisSelectionChangedEvent>(m_selectedDebris));
return; return;
} }
@@ -2893,7 +2893,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds)); std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
clearEntitySelection(); clearEntitySelection();
clearScrapSelection(); clearDebrisSelection();
} }
} }
} }
@@ -3115,7 +3115,7 @@ void GameWorldView::resetForNewGame()
std::make_shared<DeconstructModeChangedEvent>(false)); std::make_shared<DeconstructModeChangedEvent>(false));
m_selectedBuildingIds.clear(); m_selectedBuildingIds.clear();
clearEntitySelection(); clearEntitySelection();
clearScrapSelection(); clearDebrisSelection();
m_copiedConfig = std::nullopt; m_copiedConfig = std::nullopt;
m_copyConfigFlashes.clear(); m_copyConfigFlashes.clear();
m_boxSelecting = false; m_boxSelecting = false;
@@ -3151,7 +3151,7 @@ void GameWorldView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
{ {
// Endpoint offset is a fraction of the target's visual size (REQ-SHP-FIRING-BEAM): // Endpoint offset is a fraction of the target's visual size (REQ-SHP-FIRING-BEAM):
// half a ship's rendered radius, half a station's shorter footprint side, or // half a ship's rendered radius, half a station's shorter footprint side, or
// half a scrap pile's rendered radius (scrap is drawn at getTilePx()*0.2). // half a piece of debris's rendered radius (debris is drawn at getTilePx()*0.2).
float maxRadius = 0.125f; float maxRadius = 0.125f;
if (m_sim->getAdmin().isValid(event->target) if (m_sim->getAdmin().isValid(event->target)
&& m_sim->getAdmin().hasAll<StationBodyComponent>(event->target)) && m_sim->getAdmin().hasAll<StationBodyComponent>(event->target))
@@ -3161,7 +3161,7 @@ void GameWorldView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
maxRadius = shorter / 2.0f; maxRadius = shorter / 2.0f;
} }
else if (m_sim->getAdmin().isValid(event->target) else if (m_sim->getAdmin().isValid(event->target)
&& m_sim->getAdmin().hasAll<ScrapDataComponent>(event->target)) && m_sim->getAdmin().hasAll<DebrisComponent>(event->target))
{ {
maxRadius = 0.1f; maxRadius = 0.1f;
} }

View File

@@ -128,7 +128,7 @@ private:
void drawCopyConfigFeedback(QPainter& painter); void drawCopyConfigFeedback(QPainter& painter);
void drawStations(QPainter& painter); void drawStations(QPainter& painter);
void drawBeltItems(QPainter& painter); void drawBeltItems(QPainter& painter);
void drawScrap(QPainter& painter); void drawDebris(QPainter& painter);
void drawShips(QPainter& painter); void drawShips(QPainter& painter);
void drawHpBar(QPainter& painter, qreal left, qreal top, qreal width, void drawHpBar(QPainter& painter, qreal left, qreal top, qreal width,
float fraction, bool isEnemy); float fraction, bool isEnemy);
@@ -206,13 +206,13 @@ private:
void placeBlueprintAtTile(QPoint center); void placeBlueprintAtTile(QPoint center);
std::optional<QVector2D> entityPosition(entt::entity entity) const; std::optional<QVector2D> entityPosition(entt::entity entity) const;
// Clears the scrap selection, emitting an empty ScrapSelectionChangedEvent when // Clears the debris selection, emitting an empty DebrisSelectionChangedEvent when
// it was non-empty (REQ-UI-SCRAP-CLICK-SELECT). Used when another selection // it was non-empty (REQ-UI-DEBRIS-CLICK-SELECT). Used when another selection
// category takes over. // category takes over.
void clearScrapSelection(); void clearDebrisSelection();
// Drops despawned or fully-collected piles from the scrap selection and re-emits // Drops despawned or fully-collected debris from the selection and re-emits
// when it changed (REQ-UI-SCRAP-CLICK-SELECT). Called each frame from onFrame(). // when it changed (REQ-UI-DEBRIS-CLICK-SELECT). Called each frame from onFrame().
void pruneDespawnedScrap(); void pruneDespawnedDebris();
// Clears the actor selection, emitting an empty EntitySelectionChangedEvent when it was // Clears the actor selection, emitting an empty EntitySelectionChangedEvent when it was
// non-empty (REQ-UI-ENTITY-CLICK-SELECT). Used when buildings take over. // non-empty (REQ-UI-ENTITY-CLICK-SELECT). Used when buildings take over.
void clearEntitySelection(); void clearEntitySelection();
@@ -361,7 +361,7 @@ private:
std::vector<BuildingId> m_selectedBuildingIds; std::vector<BuildingId> m_selectedBuildingIds;
std::vector<entt::entity> m_selectedEntities; std::vector<entt::entity> m_selectedEntities;
std::vector<entt::entity> m_selectedScrap; std::vector<entt::entity> m_selectedDebris;
bool m_boxSelecting; bool m_boxSelecting;
QPoint m_boxStartTile; QPoint m_boxStartTile;
QPoint m_boxCurrentTile; QPoint m_boxCurrentTile;

View File

@@ -40,7 +40,7 @@
#include "RecipeSelectionDialog.h" #include "RecipeSelectionDialog.h"
#include "RecipeSelectionRequestedEvent.h" #include "RecipeSelectionRequestedEvent.h"
#include "Rotation.h" #include "Rotation.h"
#include "ScrapSystem.h" #include "DebrisSystem.h"
#include "ShipLayoutPreview.h" #include "ShipLayoutPreview.h"
#include "Simulation.h" #include "Simulation.h"
#include "WeaponComponent.h" #include "WeaponComponent.h"
@@ -225,7 +225,7 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& id
// A building selection is exclusive: it supersedes any field selection — // A building selection is exclusive: it supersedes any field selection —
// actors and scrap (REQ-UI-SELECTION-CATEGORIES). // actors and scrap (REQ-UI-SELECTION-CATEGORIES).
clearEntityDisplay(); clearEntityDisplay();
m_selectedScrap.clear(); m_selectedDebris.clear();
m_scrapLabel->hide(); m_scrapLabel->hide();
} }
rebuild(); rebuild();
@@ -667,19 +667,19 @@ void SelectedBuildingPanel::handleEvent(
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason) void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
{ {
if (!m_selectedEntities.empty() || !m_selectedScrap.empty()) if (!m_selectedEntities.empty() || !m_selectedDebris.empty())
{ {
// Field selection. Keep the live values current: the single-actor stats panel, // Field selection. Keep the live values current: the single-actor stats panel,
// the standalone scrap total, or the count summary (whose scrap line shrinks as // the single-debris stats panel (whose Scrap row shrinks as it is collected), or
// piles are collected) — matching the layout chosen by buildFieldSelection() // the count summary (whose Scrap line shrinks likewise) — matching the layout
// (REQ-UI-SHIP-STATS-PANEL, REQ-UI-SCRAP-PANEL). // chosen by buildFieldSelection() (REQ-UI-SHIP-STATS-PANEL, REQ-UI-DEBRIS-PANEL).
if (m_selectedEntities.size() == 1 && m_selectedScrap.empty()) if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
{ {
refreshEntityStats(); refreshEntityStats();
} }
else if (m_selectedEntities.empty()) else if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
{ {
refreshScrapTotal(); buildDebrisSingle();
} }
else else
{ {
@@ -936,7 +936,7 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectionCha
void SelectedBuildingPanel::buildFieldSelection() void SelectedBuildingPanel::buildFieldSelection()
{ {
if (m_selectedEntities.empty() && m_selectedScrap.empty()) if (m_selectedEntities.empty() && m_selectedDebris.empty())
{ {
// Nothing in the field category. Fall back to empty unless buildings own the panel. // Nothing in the field category. Fall back to empty unless buildings own the panel.
clearEntityDisplay(); clearEntityDisplay();
@@ -953,10 +953,11 @@ void SelectedBuildingPanel::buildFieldSelection()
EntityAdmin& admin = m_sim->getAdmin(); EntityAdmin& admin = m_sim->getAdmin();
// Full single-actor stats are shown only for a lone actor with no scrap. As soon as // A full single-object stats panel is shown only for a lone field object: one actor
// the selection holds more than one object (multiple actors, or an actor plus scrap), // with no debris, or one piece of debris with no actors. As soon as the selection holds
// the panel switches to the compact count summary (REQ-UI-FIELD-MULTI-SELECTION). // more than one object (multiple actors, multiple debris, or actors plus debris), the
if (m_selectedEntities.size() == 1 && m_selectedScrap.empty()) // panel switches to the compact count summary (REQ-UI-FIELD-MULTI-SELECTION).
if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
{ {
m_entitySummaryLabel->hide(); m_entitySummaryLabel->hide();
m_scrapLabel->hide(); m_scrapLabel->hide();
@@ -978,25 +979,36 @@ void SelectedBuildingPanel::buildFieldSelection()
return; return;
} }
m_entityTitleLabel->hide(); if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
m_entityStatsPanel->hide();
m_stationStatsLabel->hide();
if (m_selectedEntities.empty())
{ {
// Scrap only: a single "Scrap: N" line. // Single piece of debris: a "Debris" heading plus a "Scrap" stat row, styled like
// the ship/station stats panels (REQ-UI-DEBRIS-PANEL).
m_entitySummaryLabel->hide(); m_entitySummaryLabel->hide();
refreshScrapTotal(); m_entityStatsPanel->hide();
m_scrapLabel->show(); m_stationStatsLabel->hide();
buildDebrisSingle();
return; return;
} }
// Actor counts, with the scrap total appended into the same label so every line // More than one field object: a compact count summary. buildEntitySummary() appends the
// shares the same spacing. // "Debris x N" and "Scrap x N" lines when debris is part of the selection.
m_entityTitleLabel->hide();
m_entityStatsPanel->hide();
m_stationStatsLabel->hide();
m_scrapLabel->hide(); m_scrapLabel->hide();
buildEntitySummary(); buildEntitySummary();
} }
void SelectedBuildingPanel::buildDebrisSingle()
{
// "Debris" heading + a single "Scrap" stat row for the piece's remaining amount,
// mirroring the single-actor stats panels (REQ-UI-DEBRIS-PANEL).
m_entityTitleLabel->setText(tr("Debris"));
m_entityTitleLabel->show();
m_scrapLabel->setText(tr("Scrap: %1").arg(selectedDebrisScrapTotal()));
m_scrapLabel->show();
}
void SelectedBuildingPanel::buildEntitySummary() void SelectedBuildingPanel::buildEntitySummary()
{ {
EntityAdmin& admin = m_sim->getAdmin(); EntityAdmin& admin = m_sim->getAdmin();
@@ -1042,16 +1054,18 @@ void SelectedBuildingPanel::buildEntitySummary()
} }
// One "<type> x <count>" line per group (matching the recipe tooltip and the building // One "<type> x <count>" line per group (matching the recipe tooltip and the building
// multi-selection). No total-count header, consistent with the building panel. The // multi-selection). No total-count header, consistent with the building panel. When
// scrap total, when present, is appended as another line in the same label so the // debris is part of the selection, a "Debris x <count>" line followed by a
// line spacing is uniform (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-SCRAP-PANEL). // "Scrap x <total>" line are appended into the same label so the line spacing is
// uniform (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL).
QStringList lines; QStringList lines;
for (const QString& key : keys) for (const QString& key : keys)
{ {
lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]); lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]);
} }
if (!m_selectedScrap.empty()) if (!m_selectedDebris.empty())
{ {
lines << tr("Debris x %1").arg(static_cast<int>(m_selectedDebris.size()));
lines << scrapTotalText(); lines << scrapTotalText();
} }
m_entitySummaryLabel->setText(lines.join('\n')); m_entitySummaryLabel->setText(lines.join('\n'));
@@ -1173,36 +1187,36 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEv
} }
void SelectedBuildingPanel::handleEvent( void SelectedBuildingPanel::handleEvent(
std::shared_ptr<const ScrapSelectionChangedEvent> event) std::shared_ptr<const DebrisSelectionChangedEvent> event)
{ {
m_selectedScrap = event->scrap; m_selectedDebris = event->debris;
if (!m_selectedScrap.empty()) if (!m_selectedDebris.empty())
{ {
// Scrap is a field object: it supersedes any building selection but coexists // Debris is a field object: it supersedes any building selection but coexists
// with actors (REQ-UI-SELECTION-CATEGORIES). // with actors (REQ-UI-SELECTION-CATEGORIES).
m_selectedBuildingIds.clear(); m_selectedBuildingIds.clear();
} }
buildFieldSelection(); buildFieldSelection();
} }
QString SelectedBuildingPanel::scrapTotalText() const int SelectedBuildingPanel::selectedDebrisScrapTotal() const
{ {
// Sum the remaining amounts of the still-living selected piles (REQ-UI-SCRAP-PANEL). // Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL).
int total = 0; int total = 0;
for (const ScrapInfo& info : m_sim->getScraps().getAllScrapInfo()) for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity) if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
!= m_selectedScrap.end()) != m_selectedDebris.end())
{ {
total += info.amount; total += info.amount;
} }
} }
return tr("Scrap x %1").arg(total); return total;
} }
void SelectedBuildingPanel::refreshScrapTotal() QString SelectedBuildingPanel::scrapTotalText() const
{ {
m_scrapLabel->setText(scrapTotalText()); return tr("Scrap x %1").arg(selectedDebrisScrapTotal());
} }
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)

View File

@@ -18,7 +18,7 @@
#include "GameConfig.h" #include "GameConfig.h"
#include "PlayerCommandsAppliedEvent.h" #include "PlayerCommandsAppliedEvent.h"
#include "RecipesConfig.h" #include "RecipesConfig.h"
#include "ScrapSelectionChangedEvent.h" #include "DebrisSelectionChangedEvent.h"
#include "SelectionChangedEvent.h" #include "SelectionChangedEvent.h"
#include "ShipLayout.h" #include "ShipLayout.h"
#include "ShipsConfig.h" #include "ShipsConfig.h"
@@ -38,7 +38,7 @@ class SelectedBuildingPanel : public QWidget,
PlayerCommandsAppliedEvent, PlayerCommandsAppliedEvent,
EntitySelectionChangedEvent, EntitySelectionChangedEvent,
SelectionChangedEvent, SelectionChangedEvent,
ScrapSelectionChangedEvent, DebrisSelectionChangedEvent,
DebugDrawToggledEvent> DebugDrawToggledEvent>
{ {
Q_OBJECT Q_OBJECT
@@ -53,7 +53,7 @@ private:
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override; void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override; void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override; void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const ScrapSelectionChangedEvent> event) override; void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override; void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
private slots: private slots:
@@ -80,8 +80,9 @@ private:
void buildEmpty(); void buildEmpty();
void buildSingle(BuildingId id); void buildSingle(BuildingId id);
void buildMulti(const std::vector<BuildingId>& ids); void buildMulti(const std::vector<BuildingId>& ids);
void refreshScrapTotal(); // Summed remaining scrap across the selected debris (REQ-UI-DEBRIS-PANEL).
// "Scrap: N" for the summed remaining amount of the selected piles (REQ-UI-SCRAP-PANEL). int selectedDebrisScrapTotal() const;
// "Scrap x N" line for the multi-object summary (REQ-UI-FIELD-MULTI-SELECTION).
QString scrapTotalText() const; QString scrapTotalText() const;
void refreshBuffers(const Building* b); void refreshBuffers(const Building* b);
void refreshSiteProgress(const ConstructionSite* s); void refreshSiteProgress(const ConstructionSite* s);
@@ -117,23 +118,26 @@ private:
bool m_debugDraw = false; bool m_debugDraw = false;
// The selected ships/defence stations. Shares the "field" selection category with // The selected ships/defence stations. Shares the "field" selection category with
// scrap (m_selectedScrap): both can be non-empty at once (REQ-UI-SELECTION-CATEGORIES). // debris (m_selectedDebris): both can be non-empty at once (REQ-UI-SELECTION-CATEGORIES).
std::vector<entt::entity> m_selectedEntities; std::vector<entt::entity> m_selectedEntities;
ShipStatsPanel* m_entityStatsPanel; ShipStatsPanel* m_entityStatsPanel;
QLabel* m_entityTitleLabel; QLabel* m_entityTitleLabel;
QLabel* m_stationStatsLabel; QLabel* m_stationStatsLabel;
QLabel* m_entitySummaryLabel; QLabel* m_entitySummaryLabel;
std::vector<entt::entity> m_selectedScrap; std::vector<entt::entity> m_selectedDebris;
// Shows the debris "Scrap" stat row (single selection) — the scrap total for the
// multi-object summary lives in m_entitySummaryLabel instead.
QLabel* m_scrapLabel; QLabel* m_scrapLabel;
// Renders the combined field selection (actors + scrap): a single-actor stats panel // Renders the combined field selection (actors + debris): a single-object stats panel
// or a multi-actor summary, plus the scrap total when scrap is also selected // (ship, station, or debris) or a multi-object count summary that appends the debris
// (REQ-UI-FIELD-MULTI-SELECTION). // count and scrap total when debris is also selected (REQ-UI-FIELD-MULTI-SELECTION).
void buildFieldSelection(); void buildFieldSelection();
void buildEntityShip(entt::entity entity); void buildEntityShip(entt::entity entity);
void buildEntityStation(entt::entity entity); void buildEntityStation(entt::entity entity);
void buildEntitySummary(); void buildEntitySummary();
void buildDebrisSingle();
void refreshEntityStats(); void refreshEntityStats();
void clearEntityDisplay(); void clearEntityDisplay();
}; };