Rename ship/station scrap drop entities to "debris"
This commit is contained in:
@@ -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 = [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -211,8 +211,8 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- REQ-RES-SCRAP-DROP: Destroyed ships (both player and enemy) and destroyed defence stations (both player and enemy) drop scrap at their location. The scrap amount per ship is derived from the ship's threat cost (REQ-MOD-THREAT) for its as-built layout, multiplied by `world.toml [world].scrap_per_threat` (default 0.01) and rounded to the nearest integer (at least 1 for any ship whose threat cost is greater than 0); for stations it is defined as `stations.toml [player_station].scrap_drop_formula` and `[enemy_station].scrap_drop_formula`. A scrap drop carries an amount; salvage modules collect it one scrap per cycle (REQ-SHP-SALVAGE), and the drop is removed from the world once its remaining amount reaches zero or `world.toml [world].scrap_despawn_seconds` seconds have elapsed since it was dropped, whichever comes first.
|
- REQ-RES-DEBRIS-DROP: Destroyed ships (both player and enemy) and destroyed defence stations (both player and enemy) drop a piece of **debris** at their location. A piece of debris carries a scrap amount. For a ship this amount is derived from the ship's threat cost (REQ-MOD-THREAT) for its as-built layout, multiplied by `world.toml [world].scrap_per_threat` (default 0.01) and rounded to the nearest integer (at least 1 for any ship whose threat cost is greater than 0); for stations it is defined as `stations.toml [player_station].scrap_drop_formula` and `[enemy_station].scrap_drop_formula`. Salvage modules collect from a piece of debris one scrap per cycle (REQ-SHP-SALVAGE), and the debris is removed from the world once its remaining scrap amount reaches zero or `world.toml [world].debris_despawn_seconds` seconds have elapsed since it was dropped, whichever comes first.
|
||||||
- REQ-RES-SCRAP-COLLECT: Scrap is collected by salvage ships and delivered to a Salvage Bay on the asteroid. From there it can be fed via belt into a smelter (same output as ore) or a Reprocessing Plant.
|
- REQ-RES-SCRAP-COLLECT: Scrap is collected from debris by salvage ships and delivered to a Salvage Bay on the asteroid. From there it can be fed via belt into a smelter (same output as ore) or a Reprocessing Plant.
|
||||||
|
|
||||||
## Ships
|
## Ships
|
||||||
|
|
||||||
@@ -231,14 +231,14 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
|||||||
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
||||||
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
||||||
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
||||||
- REQ-SHP-FIRING-BEAM: Each weapon fire event (REQ-SHP-FIRING), repair-tool activation (REQ-SHP-REPAIR), and salvage activation (REQ-SHP-SALVAGE) produces a visual beam drawn from the acting ship's position to the target for 0.3 seconds; repair and salvage beams have the same duration as weapon beams. The beam is rendered in the tool type's beam color from `visuals.toml` (a distinct color for weapon, repair, and salvage beams). The beam endpoint is not the target's center but a point randomly offset from it: the offset direction is uniformly random and the offset magnitude is uniformly random up to half the target's visual size (for ships: half their rendered radius; for buildings/stations: half the shorter side of their tile footprint, in world units; for a scrap pile: half its rendered size). The offset is chosen once per activation event and held fixed for the beam's lifetime. The beam is a pure rendering effect and has no simulation state (does not block movement, does not re-apply its effect over its lifetime). Beams follow the acting ship and target positions if either moves during the 0.3-second window. The beam is rendered for its full 0.3-second duration even if the acting ship or target is destroyed before it expires.
|
- REQ-SHP-FIRING-BEAM: Each weapon fire event (REQ-SHP-FIRING), repair-tool activation (REQ-SHP-REPAIR), and salvage activation (REQ-SHP-SALVAGE) produces a visual beam drawn from the acting ship's position to the target for 0.3 seconds; repair and salvage beams have the same duration as weapon beams. The beam is rendered in the tool type's beam color from `visuals.toml` (a distinct color for weapon, repair, and salvage beams). The beam endpoint is not the target's center but a point randomly offset from it: the offset direction is uniformly random and the offset magnitude is uniformly random up to half the target's visual size (for ships: half their rendered radius; for buildings/stations: half the shorter side of their tile footprint, in world units; for a piece of debris: half its rendered size). The offset is chosen once per activation event and held fixed for the beam's lifetime. The beam is a pure rendering effect and has no simulation state (does not block movement, does not re-apply its effect over its lifetime). Beams follow the acting ship and target positions if either moves during the 0.3-second window. The beam is rendered for its full 0.3-second duration even if the acting ship or target is destroyed before it expires.
|
||||||
- REQ-SHP-COMBAT: Ships with at least one **weapon module** (player) — engage enemy ships within sensor range. When engaging an enemy, the ship orbits it at the combat orbit radius (REQ-SHP-ORBIT) rather than approaching its center.
|
- REQ-SHP-COMBAT: Ships with at least one **weapon module** (player) — engage enemy ships within sensor range. When engaging an enemy, the ship orbits it at the combat orbit radius (REQ-SHP-ORBIT) rather than approaching its center.
|
||||||
- REQ-SHP-RALLY: After spawning, ships with weapon modules move to and orbit the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position) — at the rally orbit radius (REQ-SHP-ORBIT). While orbiting the rally point, ships still engage any enemy that enters sensor range (switching to the combat orbit per REQ-SHP-COMBAT). Every `world.toml [world].departure_interval_seconds` seconds (default 20), all ships with weapon modules currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point.
|
- REQ-SHP-RALLY: After spawning, ships with weapon modules move to and orbit the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position) — at the rally orbit radius (REQ-SHP-ORBIT). While orbiting the rally point, ships still engage any enemy that enters sensor range (switching to the combat orbit per REQ-SHP-COMBAT). Every `world.toml [world].departure_interval_seconds` seconds (default 20), all ships with weapon modules currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point.
|
||||||
- REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, navigate toward it by orbiting it at the salvage orbit radius (REQ-SHP-ORBIT); when it is within a module's `collection_range`, that module begins collecting from it, one scrap per cycle (see below). Once the ship's cargo pool is full, fly to a Salvage Bay and deliver (a direct approach, not an orbit — the ship must reach the bay); after delivery, resume patrol. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range, then resumes patrol — this applies regardless of whether the ship is targeting or carrying scrap. Ships with salvage modules are vulnerable to enemy ships while operating.
|
- REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If debris enters sensor range, navigate toward it by orbiting it at the salvage orbit radius (REQ-SHP-ORBIT); when it is within a module's `collection_range`, that module begins collecting from it, one scrap per cycle (see below). Once the ship's cargo pool is full, fly to a Salvage Bay and deliver (a direct approach, not an orbit — the ship must reach the bay); after delivery, resume patrol. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range, then resumes patrol — this applies regardless of whether the ship is targeting debris or carrying scrap. Ships with salvage modules are vulnerable to enemy ships while operating.
|
||||||
|
|
||||||
All salvage modules on a ship deposit into a single shared **cargo pool** whose size is the ship's cargo capacity stat (REQ-MOD-CARGO-CAPACITY). Each salvage module instance still runs its own collection cycle independently, with its own collection range (`collection_range`) and collection rate (`collection_rate`, in collection cycles per second). A module starts a collection cycle when it is off cooldown, the shared cargo pool has free space, and a scrap pile is within its `collection_range`. Free space is measured against the pool's current contents **plus the collection cycles already in flight toward the pool** (scrap claimed by cycles whose effect delay has not yet elapsed); each in-flight cycle is registered against the ship so that concurrent modules on the same ship never start more cycles than the remaining capacity can hold. Starting a cycle emits a collection beam toward that scrap pile (REQ-SHP-FIRING-BEAM) and begins a 0.15-second effect delay (half the beam duration); the module's cooldown of `1 / collection_rate` seconds begins at cycle start, not at effect application. When the delay expires, exactly 1 scrap is removed from the targeted pile and added to the ship's cargo pool — unless the pile has already been fully depleted or despawned, or the pool is now full, in which case the collection is silently dropped. A scrap pile worth more than 1 (REQ-RES-SCRAP-DROP) is depleted one scrap per cycle and persists, with its remaining amount decremented, until it is fully collected or despawns. A ship with multiple salvage modules can therefore run multiple collection cycles concurrently (one per ready module), and instances of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
|
All salvage modules on a ship deposit into a single shared **cargo pool** whose size is the ship's cargo capacity stat (REQ-MOD-CARGO-CAPACITY). Each salvage module instance still runs its own collection cycle independently, with its own collection range (`collection_range`) and collection rate (`collection_rate`, in collection cycles per second). A module starts a collection cycle when it is off cooldown, the shared cargo pool has free space, and a piece of debris is within its `collection_range`. Free space is measured against the pool's current contents **plus the collection cycles already in flight toward the pool** (scrap claimed by cycles whose effect delay has not yet elapsed); each in-flight cycle is registered against the ship so that concurrent modules on the same ship never start more cycles than the remaining capacity can hold. Starting a cycle emits a collection beam toward that debris (REQ-SHP-FIRING-BEAM) and begins a 0.15-second effect delay (half the beam duration); the module's cooldown of `1 / collection_rate` seconds begins at cycle start, not at effect application. When the delay expires, exactly 1 scrap is removed from the targeted debris and added to the ship's cargo pool — unless the debris has already been fully depleted or despawned, or the pool is now full, in which case the collection is silently dropped. A piece of debris worth more than 1 (REQ-RES-DEBRIS-DROP) is depleted one scrap per cycle and persists, with its remaining scrap amount decremented, until it is fully collected or despawns. A ship with multiple salvage modules can therefore run multiple collection cycles concurrently (one per ready module), and instances of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
|
||||||
|
|
||||||
Salvage collection cycles and delivery are processed regardless of which behavior the ship is currently executing; the salvage behavior only governs where the ship navigates (toward scrap, toward a Salvage Bay, or — when retreating — toward the rally point).
|
Salvage collection cycles and delivery are processed regardless of which behavior the ship is currently executing; the salvage behavior only governs where the ship navigates (toward debris, toward a Salvage Bay, or — when retreating — toward the rally point).
|
||||||
- REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — when no more urgent behavior applies, hold with the fleet (REQ-SHP-STANDBY) rather than charging the enemy, so damaged allies stay within sensor range. If a damaged player defence station or player ship enters sensor range, navigate toward it by orbiting it at the repair orbit radius (REQ-SHP-ORBIT) and repair. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range — except that it holds its ground and keeps repairing while a damaged friendly remains within sensor range (REQ-SHP-RETREAT), retreating only once there is nothing left to repair — then resumes patrol.
|
- REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — when no more urgent behavior applies, hold with the fleet (REQ-SHP-STANDBY) rather than charging the enemy, so damaged allies stay within sensor range. If a damaged player defence station or player ship enters sensor range, navigate toward it by orbiting it at the repair orbit radius (REQ-SHP-ORBIT) and repair. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range — except that it holds its ground and keeps repairing while a damaged friendly remains within sensor range (REQ-SHP-RETREAT), retreating only once there is nothing left to repair — then resumes patrol.
|
||||||
|
|
||||||
Each repair module instance operates independently: it has its own repair rate (`repair_rate`, in repair cycles per second), per-cycle heal amount (`repair_amount_hp`), and repair range (`repair_range`). A module starts a repair cycle when it is off cooldown and a valid repair target is in range. To choose the target, the module first considers the ship's current behavior-level navigation target if that target is within the module's `repair_range` and is damaged (HP above zero and below maximum HP). If those conditions are not met — because the target is out of the module's `repair_range`, already at full health, or destroyed — the module independently searches for the nearest damaged friendly (player ship or player defence station) within its own `repair_range`. If no valid target is found within range, the module idles and starts no cycle. On starting a cycle, the module emits a repair beam toward the chosen target (REQ-SHP-FIRING-BEAM) and begins a 0.15-second effect delay (half the beam duration); the module's cooldown of `1 / repair_rate` seconds begins at cycle start, not at effect application. When the delay expires, `repair_amount_hp` HP is restored to the targeted entity, clamped to its maximum HP — unless that entity is no longer damaged or has been destroyed, in which case the heal is silently dropped. A ship with multiple repair modules can therefore run multiple repair cycles concurrently, healing different targets. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves. Repair cycles are processed regardless of which behavior the ship is currently executing.
|
Each repair module instance operates independently: it has its own repair rate (`repair_rate`, in repair cycles per second), per-cycle heal amount (`repair_amount_hp`), and repair range (`repair_range`). A module starts a repair cycle when it is off cooldown and a valid repair target is in range. To choose the target, the module first considers the ship's current behavior-level navigation target if that target is within the module's `repair_range` and is damaged (HP above zero and below maximum HP). If those conditions are not met — because the target is out of the module's `repair_range`, already at full health, or destroyed — the module independently searches for the nearest damaged friendly (player ship or player defence station) within its own `repair_range`. If no valid target is found within range, the module idles and starts no cycle. On starting a cycle, the module emits a repair beam toward the chosen target (REQ-SHP-FIRING-BEAM) and begins a 0.15-second effect delay (half the beam duration); the module's cooldown of `1 / repair_rate` seconds begins at cycle start, not at effect application. When the delay expires, `repair_amount_hp` HP is restored to the targeted entity, clamped to its maximum HP — unless that entity is no longer damaged or has been destroyed, in which case the heal is silently dropped. A ship with multiple repair modules can therefore run multiple repair cycles concurrently, healing different targets. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves. Repair cycles are processed regardless of which behavior the ship is currently executing.
|
||||||
@@ -292,7 +292,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
|||||||
- **Multiple recipes**: if an item type can be produced by more than one non-reprocessing recipe (miner, smelter, or assembler), its threat value is the **maximum** across **all** such eligible recipes, and the threat is committed only once every eligible recipe is computable (so a shallow shortcut recipe that resolves earlier than a deeper base recipe cannot lower the item's threat). The reprocessing path is only used when no other recipe exists. If recipe cycles prevent full resolution, the max over the currently computable subset is used as a fallback.
|
- **Multiple recipes**: if an item type can be produced by more than one non-reprocessing recipe (miner, smelter, or assembler), its threat value is the **maximum** across **all** such eligible recipes, and the threat is committed only once every eligible recipe is computable (so a shallow shortcut recipe that resolves earlier than a deeper base recipe cannot lower the item's threat). The reprocessing path is only used when no other recipe exists. If recipe cycles prevent full resolution, the max over the currently computable subset is used as a fallback.
|
||||||
- **Scrap-consuming recipe fallback**: a non-reprocessing recipe that takes `scrap` as an input participates in an item's threat computation only if no scrap-free recipe (miner, smelter, or assembler) produces that item. This mirrors the reprocessing fallback rule and prevents the scrap-to-ingot smelter recipe from inflating basic material threats via the max rule.
|
- **Scrap-consuming recipe fallback**: a non-reprocessing recipe that takes `scrap` as an input participates in an item's threat computation only if no scrap-free recipe (miner, smelter, or assembler) produces that item. This mirrors the reprocessing fallback rule and prevents the scrap-to-ingot smelter recipe from inflating basic material threats via the max rule.
|
||||||
|
|
||||||
- REQ-THREAT-SCRAP: The threat value of scrap is the constant `1 / world.toml [world].scrap_per_threat`. This is the exact inverse of the scrap-drop conversion in REQ-RES-SCRAP-DROP, so a destroyed ship drops scrap worth precisely its own threat cost. Because scrap threat is now a fixed constant, it no longer depends on any ship's threat cost, removing the potential circularity with REQ-MOD-THREAT for ships built from reprocessing-only materials.
|
- REQ-THREAT-SCRAP: The threat value of scrap is the constant `1 / world.toml [world].scrap_per_threat`. This is the exact inverse of the scrap conversion in REQ-RES-DEBRIS-DROP, so a destroyed ship drops debris worth precisely its own threat cost. Because scrap threat is now a fixed constant, it no longer depends on any ship's threat cost, removing the potential circularity with REQ-MOD-THREAT for ships built from reprocessing-only materials.
|
||||||
- REQ-MOD-STAT-CALC: For each stat (on the ship hull or on a capability module instance), the final value is computed as: `final = base × total_multiplier + total_additive`, where:
|
- REQ-MOD-STAT-CALC: For each stat (on the ship hull or on a capability module instance), the final value is computed as: `final = base × total_multiplier + total_additive`, where:
|
||||||
- `base` is the stat's base value — the hull stat value (for hull stats) or the capability module's base stat value (for capability module stats).
|
- `base` is the stat's base value — the hull stat value (for hull stats) or the capability module's base stat value (for capability module stats).
|
||||||
- `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all passive module instances. Each m_i is the module's multiplicative modifier value.
|
- `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all passive module instances. Each m_i is the module's multiplicative modifier value.
|
||||||
@@ -522,11 +522,11 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
|
|
||||||
### Selected Building Panel
|
### Selected Building Panel
|
||||||
|
|
||||||
- REQ-UI-EMPTY-SELECTION: When nothing is selected (no building, construction site, ship, defence station, or scrap pile), the panel is empty.
|
- REQ-UI-EMPTY-SELECTION: When nothing is selected (no building, construction site, ship, defence station, or piece of debris), the panel is empty.
|
||||||
- REQ-UI-SELECTION-CATEGORIES: **Selection categories and precedence.** Every selectable object belongs to one of two mutually exclusive selection categories: **buildings** (buildings and construction sites) and **field objects** (ships and defence stations — player or enemy — together with scrap piles). A single selection holds objects from only one category at a time. Field objects of different kinds may be selected together (e.g. several ships plus scrap piles, freely mixing player and enemy actors). Buildings are exclusive and take precedence — **buildings win**: selecting a building (by click, Ctrl+click, or a box-drag covering at least one building) clears any field selection and yields a buildings-only selection, and conversely selecting any field object clears any building selection. Point hit-testing prefers a building over a coincident field object, and among field objects prefers an actor (ship or defence station) over a coincident scrap pile (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-SCRAP-CLICK-SELECT).
|
- REQ-UI-SELECTION-CATEGORIES: **Selection categories and precedence.** Every selectable object belongs to one of two mutually exclusive selection categories: **buildings** (buildings and construction sites) and **field objects** (ships and defence stations — player or enemy — together with debris). A single selection holds objects from only one category at a time. Field objects of different kinds may be selected together (e.g. several ships plus debris, freely mixing player and enemy actors). Buildings are exclusive and take precedence — **buildings win**: selecting a building (by click, Ctrl+click, or a box-drag covering at least one building) clears any field selection and yields a buildings-only selection, and conversely selecting any field object clears any building selection. Point hit-testing prefers a building over a coincident field object, and among field objects prefers an actor (ship or defence station) over a coincident piece of debris (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-CLICK-SELECT).
|
||||||
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
||||||
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
||||||
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or scrap piles selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
|
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
||||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button grid); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-BUTTON). Construction sites count at their building type's full placement cost regardless of construction progress.
|
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button grid); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-BUTTON). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||||
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
||||||
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
||||||
@@ -534,21 +534,21 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- For a **recipe** (Miner or Assembler): the recipe name; the name and quantity of each input item (no inputs are listed for miner recipes, which consume nothing); the completion time (`duration_seconds`); and the name and quantity of the produced output item.
|
- For a **recipe** (Miner or Assembler): the recipe name; the name and quantity of each input item (no inputs are listed for miner recipes, which consume nothing); the completion time (`duration_seconds`); and the name and quantity of the produced output item.
|
||||||
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
||||||
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
||||||
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with scrap piles and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a scrap pile adds to or establishes a field selection (REQ-UI-SCRAP-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or scrap pile) clears the selection.
|
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with debris and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a piece of debris adds to or establishes a field selection (REQ-UI-DEBRIS-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or piece of debris) clears the selection.
|
||||||
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no scrap is selected, the selected building panel shows a **ship stats panel**. (If scrap is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selected building panel shows a **ship stats panel**. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
||||||
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:
|
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:
|
||||||
- **Retreating** — the ship is retreating (REQ-SHP-RETREAT).
|
- **Retreating** — the ship is retreating (REQ-SHP-RETREAT).
|
||||||
- **Engaging** — the ship is engaging a combat target (player: REQ-SHP-COMBAT; enemy: REQ-SHP-ENEMY-AI).
|
- **Engaging** — the ship is engaging a combat target (player: REQ-SHP-COMBAT; enemy: REQ-SHP-ENEMY-AI).
|
||||||
- **Salvaging** — the ship is executing salvage navigation: seeking scrap, collecting, or delivering to a Salvage Bay (REQ-SHP-SALVAGE).
|
- **Salvaging** — the ship is executing salvage navigation: seeking debris, collecting, or delivering to a Salvage Bay (REQ-SHP-SALVAGE).
|
||||||
- **Repairing** — the ship is navigating to a repair target (REQ-SHP-REPAIR).
|
- **Repairing** — the ship is navigating to a repair target (REQ-SHP-REPAIR).
|
||||||
- **Rallying** — the ship is moving to or orbiting the rally point (REQ-SHP-RALLY).
|
- **Rallying** — the ship is moving to or orbiting the rally point (REQ-SHP-RALLY).
|
||||||
- **Standby** — the ship is holding with its fleet (REQ-SHP-STANDBY).
|
- **Standby** — the ship is holding with its fleet (REQ-SHP-STANDBY).
|
||||||
- **Advancing** — the ship is executing the baseline forward advance with no higher-priority behavior active (player: REQ-SHP-COMBAT advance toward the enemy; enemy: REQ-SHP-ENEMY-AI advance toward the asteroid).
|
- **Advancing** — the ship is executing the baseline forward advance with no higher-priority behavior active (player: REQ-SHP-COMBAT advance toward the enemy; enemy: REQ-SHP-ENEMY-AI advance toward the asteroid).
|
||||||
- REQ-UI-STATION-STATS-PANEL: When exactly one defence station is selected (REQ-UI-ENTITY-CLICK-SELECT) and no scrap is selected, the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate. (If scrap is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.)
|
- REQ-UI-STATION-STATS-PANEL: When exactly one defence station is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.)
|
||||||
- REQ-UI-FIELD-MULTI-SELECTION: The full single-actor stats panel (REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL) is shown only when the field selection holds exactly one actor and no scrap. Whenever the selection holds more than one object — multiple actors, or a single actor together with scrap — the panel shows a **compact summary** instead: a count per actor type, one line per type rendered as "<type> x <count>" (the same `x`-count notation as the recipe tooltip and the building multi-selection, REQ-UI-MULTI-SELECTION). Ships are grouped by schematic display name and defence stations as a group, distinguishing player from enemy. No per-actor detail and no total-actor-count header are shown (consistent with the building panel). If scrap piles are also part of the field selection (REQ-UI-SELECTION-CATEGORIES) their total is appended as a final line of the same summary (REQ-UI-SCRAP-PANEL), so all lines share uniform spacing. Building selections use REQ-UI-SINGLE-SELECTION / REQ-UI-MULTI-SELECTION instead.
|
- REQ-UI-FIELD-MULTI-SELECTION: A full single-object stats panel (REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL, REQ-UI-DEBRIS-PANEL) is shown only when the field selection holds exactly one object — one ship, one defence station, or one piece of debris. Whenever the selection holds more than one field object — multiple actors, multiple pieces of debris, or any mix of actors and debris — the panel shows a **compact summary** instead: a count per type, one line per type rendered as "<type> x <count>" (the same `x`-count notation as the recipe tooltip and the building multi-selection, REQ-UI-MULTI-SELECTION). Ships are grouped by schematic display name and defence stations as a group, distinguishing player from enemy; all selected pieces of debris are grouped into a single "Debris x <count>" line whose count is the number of selected debris pieces. No per-object detail and no total-object-count header are shown (consistent with the building panel). If debris is part of the selection, a final "Scrap x <total>" line is appended after the "Debris" line, summing the remaining scrap across all selected debris (REQ-UI-DEBRIS-PANEL), so all lines share uniform spacing. Building selections use REQ-UI-SINGLE-SELECTION / REQ-UI-MULTI-SELECTION instead.
|
||||||
- REQ-UI-SCRAP-CLICK-SELECT: The player can click any scrap pile (REQ-RES-SCRAP-DROP) in the game world to select it. Scrap piles are field objects (REQ-UI-SELECTION-CATEGORIES) and can be selected together with ships and defence stations, but not with buildings. A plain click on a scrap pile makes it the sole selection, clearing any previous selection; selecting a building clears any scrap (buildings win), and selecting a scrap pile clears any building selection. Hit-testing prefers a building over a coincident actor or scrap pile, and an actor (ship or defence station) over a coincident scrap pile: a scrap pile is selected only when no building or actor is under the cursor. A selected scrap pile that despawns or is fully collected (REQ-RES-SCRAP-DROP) is removed from the selection; if no selected object remains, the panel becomes empty (REQ-UI-EMPTY-SELECTION).
|
- REQ-UI-DEBRIS-CLICK-SELECT: The player can click any piece of debris (REQ-RES-DEBRIS-DROP) in the game world to select it. Debris are field objects (REQ-UI-SELECTION-CATEGORIES) and can be selected together with ships and defence stations, but not with buildings. A plain click on a piece of debris makes it the sole selection, clearing any previous selection; selecting a building clears any debris (buildings win), and selecting a piece of debris clears any building selection. Hit-testing prefers a building over a coincident actor or piece of debris, and an actor (ship or defence station) over a coincident piece of debris: a piece of debris is selected only when no building or actor is under the cursor. A selected piece of debris that despawns or is fully collected (REQ-RES-DEBRIS-DROP) is removed from the selection; if no selected object remains, the panel becomes empty (REQ-UI-EMPTY-SELECTION).
|
||||||
- REQ-UI-SCRAP-MULTI-SELECT: Multiple scrap piles can be selected by box-drag or by Ctrl+clicking individual piles to add or remove them, mirroring building multi-select (REQ-UI-MULTI-SELECT). Scrap shares the field-object category with ships and defence stations (REQ-UI-SELECTION-CATEGORIES), so a field selection may hold scrap piles and actors together. Ctrl+clicking a scrap pile while a field selection is active adds or removes that pile within the same selection; Ctrl+clicking a scrap pile while a building selection is active first clears the buildings and begins a field selection (buildings win). Conversely, selecting a building while a field selection is active clears it. Box-drag disambiguation follows REQ-UI-MULTI-SELECT (a box covering any building selects buildings; a box covering no building selects the ships, defence stations, and scrap piles within it).
|
- REQ-UI-DEBRIS-MULTI-SELECT: Multiple pieces of debris can be selected by box-drag or by Ctrl+clicking individual pieces to add or remove them, mirroring building multi-select (REQ-UI-MULTI-SELECT). Debris shares the field-object category with ships and defence stations (REQ-UI-SELECTION-CATEGORIES), so a field selection may hold debris and actors together. Ctrl+clicking a piece of debris while a field selection is active adds or removes that piece within the same selection; Ctrl+clicking a piece of debris while a building selection is active first clears the buildings and begins a field selection (buildings win). Conversely, selecting a building while a field selection is active clears it. Box-drag disambiguation follows REQ-UI-MULTI-SELECT (a box covering any building selects buildings; a box covering no building selects the ships, defence stations, and debris within it).
|
||||||
- REQ-UI-SCRAP-PANEL: When one or more scrap piles are selected, the selected building panel shows the **total remaining scrap amount** across all selected piles — the sum of the piles' current remaining amounts (REQ-RES-SCRAP-DROP), e.g. "Scrap x 47". The same summed-amount display is used whether one pile or many are selected; no per-pile detail and no pile count are shown. The displayed total updates as selected piles are partially collected or despawn (REQ-UI-SCRAP-CLICK-SELECT). When actors are also selected, this scrap total is shown as an additional line of the actor count summary rather than alongside a single-actor stats panel (REQ-UI-FIELD-MULTI-SELECTION).
|
- REQ-UI-DEBRIS-PANEL: When exactly one piece of debris is selected (and no actors, REQ-UI-FIELD-MULTI-SELECTION), the selected building panel shows a **debris stats panel** structured like the ship and station stats panels (REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL): a **"Debris"** heading followed by a single stat row, **"Scrap"**, showing that piece's current remaining scrap amount (REQ-RES-DEBRIS-DROP), rendered in the same label/value style as a ship hull stat row. When more than one field object is selected — multiple pieces of debris, or debris together with actors — the debris are instead summarized within the compact count summary (REQ-UI-FIELD-MULTI-SELECTION): a "Debris x <count>" line giving the number of selected debris pieces, followed by a "Scrap x <total>" line summing the remaining scrap across all selected debris. The displayed scrap value(s) update as selected debris are partially collected or despawn (REQ-UI-DEBRIS-CLICK-SELECT).
|
||||||
|
|
||||||
### Build Button Grid
|
### Build Button Grid
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
8
src/lib/ecs/component/DebrisComponent.h
Normal file
8
src/lib/ecs/component/DebrisComponent.h
Normal 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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
struct ScrapDataComponent
|
|
||||||
{
|
|
||||||
int amount;
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
78
src/lib/ecs/system/DebrisSystem.cpp
Normal file
78
src/lib/ecs/system/DebrisSystem.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/lib/eventsystem/event/DebrisSelectionChangedEvent.h
Normal file
18
src/lib/eventsystem/event/DebrisSelectionChangedEvent.h
Normal 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;
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
{
|
||||||
|
// 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_entityStatsPanel->hide();
|
m_entityStatsPanel->hide();
|
||||||
m_stationStatsLabel->hide();
|
m_stationStatsLabel->hide();
|
||||||
|
buildDebrisSingle();
|
||||||
if (m_selectedEntities.empty())
|
|
||||||
{
|
|
||||||
// Scrap only: a single "Scrap: N" line.
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
refreshScrapTotal();
|
|
||||||
m_scrapLabel->show();
|
|
||||||
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)
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user