9 Commits

62 changed files with 837 additions and 626 deletions

View File

@@ -10,7 +10,8 @@ tunnel_max_distance_tiles = 10
departure_interval_seconds = 20 departure_interval_seconds = 20
orbit_factor = 0.8 orbit_factor = 0.8
rally_orbit_radius_tiles = 5.0 rally_orbit_radius_tiles = 5.0
building_blocks_tooltip = "Building blocks are the currency for construction. Spend them to place buildings and to expand the asteroid. Produce building blocks in your factory and deliver them to the HQ on a belt to grow your stock." building_blocks_tooltip = "Building blocks are the currency for construction. Spend them to place buildings and to expand the asteroid. Produce building blocks in your assemblers and deliver them to the HQ on a belt to grow your stock."
artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game."
[regions] [regions]
asteroid_width_tiles = 60 asteroid_width_tiles = 60

View File

@@ -11,6 +11,7 @@ departure_interval_seconds = 20
orbit_factor = 0.8 orbit_factor = 0.8
rally_orbit_radius_tiles = 5.0 rally_orbit_radius_tiles = 5.0
building_blocks_tooltip = "Spend building blocks to build; deliver them to the HQ to gain more." building_blocks_tooltip = "Spend building blocks to build; deliver them to the HQ to gain more."
artifact_tooltip = "Choose the artifact reward when destroying enemy stations; collect enough to win."
[regions] [regions]
asteroid_width_tiles = 40 asteroid_width_tiles = 40

View File

@@ -97,6 +97,12 @@ All UI interactions — building selection, builder/blueprint mode transitions,
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid``GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView``BuildButtonGrid`). Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid``GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView``BuildButtonGrid`).
### Reading Simulation State
The simulation is the single source of truth for every game value (building block stock, expansion cost, threat level, tick, etc.). A UI widget that needs such a value holds the `Simulation*` it was constructed with and **pulls the value on demand** via the corresponding getter (e.g., `m_sim->getBuildingBlocksStock()`), rather than caching its own copy.
State-change events (e.g., `BuildingBlocksChangedEvent`) are treated as *refresh signals*, not as carriers of truth: a widget subscribes to the event to learn *when* the value changed and then re-reads it from the simulation to learn *what* it now is. The value carried in the event payload is not authoritative and should not be stored. This keeps a single copy of each value and avoids stale-cache bugs (a widget acting on a value that has since moved on because nothing refreshed its local copy).
## Tick Order ## Tick Order
Within a single simulation tick, subsystems run in this fixed order. The order is load-bearing for determinism and for avoiding one-tick-delay artifacts (e.g., items landing on a belt but not advancing in the same tick). Within a single simulation tick, subsystems run in this fixed order. The order is load-bearing for determinism and for avoiding one-tick-delay artifacts (e.g., items landing on a belt but not advancing in the same tick).

View File

@@ -4,7 +4,7 @@
Config files use the TOML format. The following config files drive game parameters: Config files use the TOML format. The following config files drive game parameters:
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), and an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset). - **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset), and an optional artifact tooltip string (shown as the header bar's artifact count hover tooltip, REQ-UI-ARTIFACTS-TOOLTIP; omitted when unset).
- **buildings.toml** — building block cost and construction time per building type, plus an optional tooltip description string per building type (shown as the build button's hover tooltip, REQ-UI-BUILD-TOOLTIP; omitted when unset). - **buildings.toml** — building block cost and construction time per building type, plus an optional tooltip description string per building type (shown as the build button's hover tooltip, REQ-UI-BUILD-TOOLTIP; omitted when unset).
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). An assembler recipe schematic entry may also define an optional `unlock_requires` list of prerequisite schematic ids (REQ-LOCK-PREREQ). - **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). An assembler recipe schematic entry may also define an optional `unlock_requires` list of prerequisite schematic ids (REQ-LOCK-PREREQ).
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES). - **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
@@ -424,8 +424,11 @@ The screen is divided into two columns: a main column (75% width) containing the
- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) and an asteroid expansion button (REQ-UI-EXPAND-BUTTON) to the left of the speed buttons, and game speed controls on the right. - REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) and an asteroid expansion button (REQ-UI-EXPAND-BUTTON) to the left of the speed buttons, and game speed controls on the right.
- REQ-UI-BLOCKS-TOOLTIP: The header bar's building blocks stock display (REQ-UI-HEADER) shows a hover tooltip with the descriptive text defined in `world.toml [world].building_blocks_tooltip` — intended to tell the player what building blocks are used for and how to obtain them. If the field is unset, the stock display shows no tooltip. This tooltip is distinct from the build/module button tooltips (REQ-UI-BUILD-TOOLTIP, REQ-MOD-UI-MODULE-TOOLTIP). - REQ-UI-BLOCKS-TOOLTIP: The header bar's building blocks stock display (REQ-UI-HEADER) shows a hover tooltip with the descriptive text defined in `world.toml [world].building_blocks_tooltip` — intended to tell the player what building blocks are used for and how to obtain them. If the field is unset, the stock display shows no tooltip. This tooltip is distinct from the build/module button tooltips (REQ-UI-BUILD-TOOLTIP, REQ-MOD-UI-MODULE-TOOLTIP).
- REQ-UI-ARTIFACTS-TOOLTIP: The header bar's artifact count display (REQ-UI-HEADER) shows a hover tooltip with the descriptive text defined in `world.toml [world].artifact_tooltip` — intended to tell the player what artifacts are, how they are obtained (REQ-DEF-SCHEMATIC-DROP), and that collecting `world.toml [world].artifact_win_count` of them wins the game (REQ-WIN-ARTIFACT-COUNT). If the field is unset, the artifact count display shows no tooltip. This tooltip is distinct from the building blocks tooltip (REQ-UI-BLOCKS-TOOLTIP) and the build/module button tooltips (REQ-UI-BUILD-TOOLTIP, REQ-MOD-UI-MODULE-TOOLTIP).
- REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs. - REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game. - REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 10× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
- REQ-UI-PAUSE-BORDER: While the game is paused (speed 0×, whether set via the speed controls (REQ-UI-SPEED), the Space toggle (REQ-UI-HOTKEYS), or an auto-pausing modal), a vignette border is drawn around the edges of the game world view to make the paused state hard to miss. The border is black and fades in the alpha channel from fully transparent at its inner (center-facing) edge to 50% opacity at the viewport edge, over a thickness of 100 pixels (capped at half the smaller viewport dimension on very small views).
- REQ-UI-DEMOLISH-BORDER: While demolish mode is active (REQ-UI-DEMOLISH-BUTTON, REQ-UI-HOTKEYS), a vignette border is drawn around the edges of the game world view to signal the mode, matching the geometry of the paused-state vignette (REQ-UI-PAUSE-BORDER): a 100-pixel thickness (capped at half the smaller viewport dimension on very small views) with the four sides meeting along mitred corner diagonals. It fades in the alpha channel from fully transparent at its inner (center-facing) edge to the demolish tint color at the viewport edge. The color — including its alpha, which sets the peak opacity at the viewport edge — is read from `visuals.toml [overlays].demolish_tint`, the same demolish-mode color used for the hover tint. The border is presentation-only and has no effect on the simulation. If the game is both paused and in demolish mode, both vignettes are drawn and compose over each other.
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x> Blocks`, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion. - REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x> Blocks`, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
- REQ-UI-WORLD-SIZE: The game world view occupies the full height below the header bar in the main column (75% of the screen width). - REQ-UI-WORLD-SIZE: The game world view occupies the full height below the header bar in the main column (75% of the screen width).
- REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It is divided into three equal-height panels stacked top to bottom: selected building panel (top), build button grid (middle), and blueprint panel (bottom). - REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It is divided into three equal-height panels stacked top to bottom: selected building panel (top), build button grid (middle), and blueprint panel (bottom).
@@ -442,11 +445,12 @@ The screen is divided into two columns: a main column (75% width) containing the
Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM. Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM.
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's glyph centered on the footprint (same as an operational building). Below the glyph — or centered on the footprint if the building has no glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes. - REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's glyph centered on the footprint (same as an operational building). Below the glyph — or centered on the footprint if the building has no glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port. - REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
- REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE).
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP. - REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out. - REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
- REQ-UI-HOTKEYS: Global keyboard shortcuts: - REQ-UI-HOTKEYS: Global keyboard shortcuts:
- **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed. - **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed.
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 4× (no wrap-around past 4×). - **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 10× (no wrap-around past 10×).
- **S** — decreases game speed by one step in the same sequence (no wrap-around past 0×). - **S** — decreases game speed by one step in the same sequence (no wrap-around past 0×).
- **A / D** — scroll the view left / right (REQ-UI-SCROLL). - **A / D** — scroll the view left / right (REQ-UI-SCROLL).
- **Q** — context-sensitive. If a build mode is active (builder mode or blueprint placement mode), pressing Q exits it. Otherwise, pressing Q toggles demolish mode: it enters demolish mode if inactive, or exits demolish mode if already active. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.) - **Q** — context-sensitive. If a build mode is active (builder mode or blueprint placement mode), pressing Q exits it. Otherwise, pressing Q toggles demolish mode: it enters demolish mode if inactive, or exits demolish mode if already active. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.)
@@ -560,7 +564,7 @@ A separate executable target (`balancing`) that links against `lib` but contains
- REQ-BAL-SIM-ENV: Each arena simulates a pure-space environment using the same tick-based simulation as the main game. There is no asteroid, no buildings, no belts, no wave system, and no threat accumulation. Only ships, HQs, defence stations, and combat are active. - REQ-BAL-SIM-ENV: Each arena simulates a pure-space environment using the same tick-based simulation as the main game. There is no asteroid, no buildings, no belts, no wave system, and no threat accumulation. Only ships, HQs, defence stations, and combat are active.
- REQ-BAL-SIM-AI: Ships use the same AI and stats as in the main game. Ships with no target in sensor range advance toward the enemy team's HQ. Ships that detect an enemy in sensor range engage it as in the normal game (REQ-SHP-COMBAT, REQ-SHP-ENEMY-AI). - REQ-BAL-SIM-AI: Ships use the same AI and stats as in the main game. Ships with no target in sensor range advance toward the enemy team's HQ. Ships that detect an enemy in sensor range engage it as in the normal game (REQ-SHP-COMBAT, REQ-SHP-ENEMY-AI).
- REQ-BAL-SIM-SPEED: Each arena that is not being inspected runs its simulation at maximum tick rate (as many ticks per second as the hardware allows), with no rendering. An inspected arena runs at a player-controllable game speed (same speed steps as the main game: 0×, 0.5×, 1×, 2×, 4×) with full rendering in the inspect window, defaulting to 1× on open. - REQ-BAL-SIM-SPEED: Each arena that is not being inspected runs its simulation at maximum tick rate (as many ticks per second as the hardware allows), with no rendering. An inspected arena runs at a player-controllable game speed (same speed steps as the main game: 0×, 0.5×, 1×, 2×, 10×) with full rendering in the inspect window, defaulting to 1× on open.
- REQ-BAL-SIM-PARALLEL: All arenas are simulated in parallel, each on its own thread. - REQ-BAL-SIM-PARALLEL: All arenas are simulated in parallel, each on its own thread.
- REQ-BAL-SIM-END: An arena fight ends when either team's HQ is destroyed or all ships and defence stations of one team have been destroyed. If a team has no defence stations, destroying all its ships is sufficient. When the fight ends, the simulation for that arena stops. - REQ-BAL-SIM-END: An arena fight ends when either team's HQ is destroyed or all ships and defence stations of one team have been destroyed. If a team has no defence stations, destroying all its ships is sufficient. When the fight ends, the simulation for that arena stops.
@@ -577,6 +581,6 @@ A separate executable target (`balancing`) that links against `lib` but contains
- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced). - REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended. - REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended.
- REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time. - REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time.
- REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 4×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live, including the arena's battle duration once the fight has ended (REQ-BAL-UI-WIDGET). - REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 10×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live, including the arena's battle duration once the fight has ended (REQ-BAL-UI-WIDGET).
- REQ-BAL-UI-INSPECT-VIEW: The arena view renders all tiles of the arena and displays ships, HQs, defence stations, and laser beams using the same visual elements and `visuals.toml` colors as the main game. Team 1 uses player visual styles; team 2 uses enemy visual styles. The view has a fixed zoom level — no zoom or scroll is possible. The tile size is derived so that the full arena (all tiles) fits within the view. - REQ-BAL-UI-INSPECT-VIEW: The arena view renders all tiles of the arena and displays ships, HQs, defence stations, and laser beams using the same visual elements and `visuals.toml` colors as the main game. Team 1 uses player visual styles; team 2 uses enemy visual styles. The view has a fixed zoom level — no zoom or scroll is possible. The tile size is derived so that the full arena (all tiles) fits within the view.
- REQ-BAL-UI-INSPECT-CLOSE: Closing the inspect window (via the window's close button) aborts the inspected arena's simulation. The arena widget's border returns to grey and its surviving counts are left as they were at the moment of closing. All main window buttons and controls are re-enabled. - REQ-BAL-UI-INSPECT-CLOSE: Closing the inspect window (via the window's close button) aborts the inspected arena's simulation. The arena widget's border returns to grey and its surviving counts are left as they were at the moment of closing. All main window buttons and controls are re-enabled.

View File

@@ -95,7 +95,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
updateStatus(); updateStatus();
} }
std::string ArenaStatus::TeamStatus::ehpPercentText() const std::string ArenaStatus::TeamStatus::getEhpPercentText() const
{ {
if (maxEhp <= 0.0) if (maxEhp <= 0.0)
{ {
@@ -312,7 +312,7 @@ void ArenaSimulation::requestStop()
m_stopRequested.store(true, std::memory_order_relaxed); m_stopRequested.store(true, std::memory_order_relaxed);
} }
ArenaStatus ArenaSimulation::status() const ArenaStatus ArenaSimulation::getStatus() const
{ {
std::lock_guard<std::mutex> lock(m_statusMutex); std::lock_guard<std::mutex> lock(m_statusMutex);
return m_status; return m_status;
@@ -473,42 +473,42 @@ bool ArenaSimulation::isFinished() const
return m_finished; return m_finished;
} }
int ArenaSimulation::winnerTeam() const int ArenaSimulation::getWinnerTeam() const
{ {
return m_winnerTeam; return m_winnerTeam;
} }
Tick ArenaSimulation::currentTick() const Tick ArenaSimulation::getCurrentTick() const
{ {
return m_currentTick; return m_currentTick;
} }
const ArenaConfig& ArenaSimulation::arenaConfig() const const ArenaConfig& ArenaSimulation::getArenaConfig() const
{ {
return m_arenaConfig; return m_arenaConfig;
} }
const BuildingSystem& ArenaSimulation::buildings() const const BuildingSystem& ArenaSimulation::getBuildings() const
{ {
return *m_buildingSystem; return *m_buildingSystem;
} }
const ShipSystem& ArenaSimulation::ships() const const ShipSystem& ArenaSimulation::getShips() const
{ {
return *m_shipSystem; return *m_shipSystem;
} }
const ScrapSystem& ArenaSimulation::scraps() const const ScrapSystem& ArenaSimulation::getScraps() const
{ {
return *m_scrapSystem; return *m_scrapSystem;
} }
EntityAdmin& ArenaSimulation::admin() EntityAdmin& ArenaSimulation::getAdmin()
{ {
return m_admin; return m_admin;
} }
const EntityAdmin& ArenaSimulation::admin() const const EntityAdmin& ArenaSimulation::getAdmin() const
{ {
return m_admin; return m_admin;
} }

View File

@@ -46,14 +46,14 @@ struct ArenaStatus
double threatLevel = 0.0; // accumulated threat of the team's configured ships double threatLevel = 0.0; // accumulated threat of the team's configured ships
// Remaining durability of the team's ships and defence stations (HQ // Remaining durability of the team's ships and defence stations (HQ
// excluded). currentEhp is summed live; maxEhp is the fixed full-HP // excluded). currentEhp is summed live; maxEhp is the fixed full-HP
// baseline. See ehpPercentText() for the displayed value. // baseline. See getEhpPercentText() for the displayed value.
double currentEhp = 0.0; double currentEhp = 0.0;
double maxEhp = 0.0; double maxEhp = 0.0;
std::vector<Entry> entries; // HQ first, then ships, then stations std::vector<Entry> entries; // HQ first, then ships, then stations
// Remaining EHP as a whole-number percentage ("NN%"), or "n/a" when the // Remaining EHP as a whole-number percentage ("NN%"), or "n/a" when the
// team has no ships or stations (maxEhp == 0). // team has no ships or stations (maxEhp == 0).
std::string ehpPercentText() const; std::string getEhpPercentText() const;
}; };
TeamStatus teams[2]; TeamStatus teams[2];
@@ -78,17 +78,17 @@ public:
void tickOnce(); void tickOnce();
std::vector<BeamFiredEvent> drainBeamFiredEvents(); std::vector<BeamFiredEvent> drainBeamFiredEvents();
ArenaStatus status() const; ArenaStatus getStatus() const;
bool isFinished() const; bool isFinished() const;
int winnerTeam() const; int getWinnerTeam() const;
Tick currentTick() const; Tick getCurrentTick() const;
const ArenaConfig& arenaConfig() const; const ArenaConfig& getArenaConfig() const;
const BuildingSystem& buildings() const; const BuildingSystem& getBuildings() const;
const ShipSystem& ships() const; const ShipSystem& getShips() const;
const ScrapSystem& scraps() const; const ScrapSystem& getScraps() const;
EntityAdmin& admin(); EntityAdmin& getAdmin();
const EntityAdmin& admin() const; const EntityAdmin& getAdmin() const;
private: private:
BuildingId allocateBuildingId(); BuildingId allocateBuildingId();

View File

@@ -73,7 +73,7 @@ void ArenaView::setGameSpeed(double multiplier)
std::make_shared<GameSpeedChangedEvent>(multiplier)); std::make_shared<GameSpeedChangedEvent>(multiplier));
} }
double ArenaView::gameSpeed() const double ArenaView::getGameSpeed() const
{ {
return m_gameSpeedMultiplier; return m_gameSpeedMultiplier;
} }
@@ -121,7 +121,7 @@ void ArenaView::onFrame()
// Expire old beams. Lifetime is measured in game ticks so beams stay // Expire old beams. Lifetime is measured in game ticks so beams stay
// visible while the simulation is paused or slowed (REQ-SHP-FIRING-BEAM). // visible while the simulation is paused or slowed (REQ-SHP-FIRING-BEAM).
{ {
const Tick now = m_sim->currentTick(); const Tick now = m_sim->getCurrentTick();
std::vector<ActiveBeam> live; std::vector<ActiveBeam> live;
for (const ActiveBeam& b : m_activeBeams) for (const ActiveBeam& b : m_activeBeams)
{ {
@@ -144,16 +144,16 @@ void ArenaView::onFrame()
void ArenaView::handleEvent(std::shared_ptr<const BeamFiredEvent> event) void ArenaView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
{ {
float maxRadius = 0.125f; float maxRadius = 0.125f;
if (m_sim->admin().isValid(event->target) if (m_sim->getAdmin().isValid(event->target)
&& m_sim->admin().hasAll<StationBodyComponent>(event->target)) && m_sim->getAdmin().hasAll<StationBodyComponent>(event->target))
{ {
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target); const StationBodyComponent& sb = m_sim->getAdmin().get<StationBodyComponent>(event->target);
const int shorter = std::min(sb.footprint.width(), const int shorter = std::min(sb.footprint.width(),
sb.footprint.height()); sb.footprint.height());
maxRadius = shorter / 2.0f; maxRadius = shorter / 2.0f;
} }
else if (m_sim->admin().isValid(event->target) else if (m_sim->getAdmin().isValid(event->target)
&& m_sim->admin().hasAll<ScrapDataComponent>(event->target)) && m_sim->getAdmin().hasAll<ScrapDataComponent>(event->target))
{ {
maxRadius = 0.1f; maxRadius = 0.1f;
} }
@@ -192,9 +192,9 @@ void ArenaView::paintGL()
// Coordinate helpers // Coordinate helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
float ArenaView::tilePx() const float ArenaView::getTilePx() const
{ {
const ArenaConfig& ac = m_sim->arenaConfig(); const ArenaConfig& ac = m_sim->getArenaConfig();
const int totalWidth = ac.playerBufferWidth_tiles const int totalWidth = ac.playerBufferWidth_tiles
+ ac.contestZoneWidth_tiles + ac.contestZoneWidth_tiles
+ ac.enemyBufferWidth_tiles; + ac.enemyBufferWidth_tiles;
@@ -209,8 +209,8 @@ float ArenaView::tilePx() const
QPointF ArenaView::worldToWidget(QVector2D worldPos) const QPointF ArenaView::worldToWidget(QVector2D worldPos) const
{ {
return QPointF( return QPointF(
static_cast<qreal>(worldPos.x() * tilePx()), static_cast<qreal>(worldPos.x() * getTilePx()),
static_cast<qreal>(worldPos.y() * tilePx())); static_cast<qreal>(worldPos.y() * getTilePx()));
} }
QPointF ArenaView::tileToWidget(QPoint tile) const QPointF ArenaView::tileToWidget(QPoint tile) const
@@ -223,21 +223,21 @@ QRectF ArenaView::tileRect(QPoint tile) const
{ {
const QPointF tl = tileToWidget(tile); const QPointF tl = tileToWidget(tile);
return QRectF(tl.x(), tl.y(), return QRectF(tl.x(), tl.y(),
static_cast<qreal>(tilePx()), static_cast<qreal>(tilePx())); static_cast<qreal>(getTilePx()), static_cast<qreal>(getTilePx()));
} }
std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
{ {
if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity)) if (!m_sim->getAdmin().isValid(entity) || !m_sim->getAdmin().hasAll<PositionComponent>(entity))
{ {
return std::nullopt; return std::nullopt;
} }
return m_sim->admin().get<PositionComponent>(entity).value; return m_sim->getAdmin().get<PositionComponent>(entity).value;
} }
QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const
{ {
const float px = tilePx(); const float px = getTilePx();
if (px < 0.001f) { return QVector2D(0.0f, 0.0f); } if (px < 0.001f) { return QVector2D(0.0f, 0.0f); }
return QVector2D(static_cast<float>(widgetPt.x()) / px, return QVector2D(static_cast<float>(widgetPt.x()) / px,
static_cast<float>(widgetPt.y()) / px); static_cast<float>(widgetPt.y()) / px);
@@ -248,7 +248,7 @@ void ArenaView::mousePressEvent(QMouseEvent* event)
if (event->button() == Qt::LeftButton) if (event->button() == Qt::LeftButton)
{ {
const QVector2D worldPos = widgetToWorld(event->pos()); const QVector2D worldPos = widgetToWorld(event->pos());
entt::entity hit = entityAtWorldPos(m_sim->admin(), worldPos); entt::entity hit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
if (hit != entt::null) if (hit != entt::null)
{ {
@@ -282,7 +282,7 @@ void ArenaView::keyPressEvent(QKeyEvent* event)
void ArenaView::drawTiles(QPainter& painter) void ArenaView::drawTiles(QPainter& painter)
{ {
const ArenaConfig& ac = m_sim->arenaConfig(); const ArenaConfig& ac = m_sim->getArenaConfig();
const int totalWidth = ac.playerBufferWidth_tiles const int totalWidth = ac.playerBufferWidth_tiles
+ ac.contestZoneWidth_tiles + ac.contestZoneWidth_tiles
+ ac.enemyBufferWidth_tiles; + ac.enemyBufferWidth_tiles;
@@ -300,7 +300,7 @@ void ArenaView::drawTiles(QPainter& painter)
void ArenaView::drawBuildings(QPainter& painter) void ArenaView::drawBuildings(QPainter& painter)
{ {
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
const std::map<BuildingType, BuildingVisuals>::const_iterator it = const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(b.type); m_visuals->buildings.find(b.type);
@@ -315,8 +315,8 @@ void ArenaView::drawBuildings(QPainter& painter)
const QPointF tl = tileToWidget(b.anchor); const QPointF tl = tileToWidget(b.anchor);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(tilePx()), b.footprint.width() * static_cast<qreal>(getTilePx()),
b.footprint.height() * static_cast<qreal>(tilePx())); b.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1)); painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -332,8 +332,8 @@ void ArenaView::drawBuildings(QPainter& painter)
void ArenaView::drawScrap(QPainter& painter) void ArenaView::drawScrap(QPainter& painter)
{ {
const float r = tilePx() * 0.2f; const float r = getTilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo()) for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo())
{ {
const QPointF center = worldToWidget(scrap.position); const QPointF center = worldToWidget(scrap.position);
painter.setBrush(QColor(128, 110, 90)); painter.setBrush(QColor(128, 110, 90));
@@ -345,7 +345,7 @@ void ArenaView::drawScrap(QPainter& painter)
void ArenaView::drawStations(QPainter& painter) void ArenaView::drawStations(QPainter& painter)
{ {
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( m_sim->getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h) [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h)
{ {
const BuildingType visType = f.isEnemy const BuildingType visType = f.isEnemy
@@ -364,8 +364,8 @@ void ArenaView::drawStations(QPainter& painter)
const QPointF tl = tileToWidget(sb.anchor); const QPointF tl = tileToWidget(sb.anchor);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
sb.footprint.width() * static_cast<qreal>(tilePx()), sb.footprint.width() * static_cast<qreal>(getTilePx()),
sb.footprint.height() * static_cast<qreal>(tilePx())); sb.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1)); painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -374,7 +374,7 @@ void ArenaView::drawStations(QPainter& painter)
if (h.maxHp > 0.0f) if (h.maxHp > 0.0f)
{ {
const float fraction = std::max(0.0f, h.hp / h.maxHp); const float fraction = std::max(0.0f, h.hp / h.maxHp);
const qreal barH = static_cast<qreal>(tilePx()) * 0.12; const qreal barH = static_cast<qreal>(getTilePx()) * 0.12;
const qreal barY = bboxRect.bottom() + 1.0; const qreal barY = bboxRect.bottom() + 1.0;
const qreal barW = bboxRect.width(); const qreal barW = bboxRect.width();
painter.fillRect(QRectF(bboxRect.left(), barY, barW, barH), painter.fillRect(QRectF(bboxRect.left(), barY, barW, barH),
@@ -394,7 +394,7 @@ void ArenaView::drawStations(QPainter& painter)
void ArenaView::drawShips(QPainter& painter) void ArenaView::drawShips(QPainter& painter)
{ {
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>( FactionComponent, HealthComponent>(
[&](entt::entity e, const ShipIdentityComponent& si, [&](entt::entity e, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& facing, const PositionComponent& pos, const FacingComponent& facing,
@@ -408,8 +408,8 @@ void ArenaView::drawShips(QPainter& painter)
const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians)); const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians));
const QVector2D perp(-dir.y(), dir.x()); const QVector2D perp(-dir.y(), dir.x());
const float fwd = tilePx() * 0.45f; const float fwd = getTilePx() * 0.45f;
const float side = tilePx() * 0.25f; const float side = getTilePx() * 0.25f;
QPolygonF tri; QPolygonF tri;
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd), tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
@@ -427,7 +427,7 @@ void ArenaView::drawShips(QPainter& painter)
{ {
const float fraction = std::max(0.0f, h.hp / h.maxHp); const float fraction = std::max(0.0f, h.hp / h.maxHp);
const qreal barW = static_cast<qreal>(fwd) * 2.0; const qreal barW = static_cast<qreal>(fwd) * 2.0;
const qreal barH = static_cast<qreal>(tilePx()) * 0.12; const qreal barH = static_cast<qreal>(getTilePx()) * 0.12;
const qreal barX = center.x() - static_cast<qreal>(fwd); const qreal barX = center.x() - static_cast<qreal>(fwd);
const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0; const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0;
painter.fillRect(QRectF(barX, barY, barW, barH), QColor(60, 60, 60)); painter.fillRect(QRectF(barX, barY, barW, barH), QColor(60, 60, 60));
@@ -437,7 +437,7 @@ void ArenaView::drawShips(QPainter& painter)
if (m_selectedEntity.has_value() && *m_selectedEntity == e) if (m_selectedEntity.has_value() && *m_selectedEntity == e)
{ {
const qreal radius = static_cast<qreal>(tilePx()) * 0.55; const qreal radius = static_cast<qreal>(getTilePx()) * 0.55;
painter.setPen(QPen(QColor(255, 255, 0), 2)); painter.setPen(QPen(QColor(255, 255, 0), 2));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.drawEllipse(center, radius, radius); painter.drawEllipse(center, radius, radius);
@@ -448,7 +448,7 @@ void ArenaView::drawShips(QPainter& painter)
void ArenaView::drawDebugSensorRanges(QPainter& painter) void ArenaView::drawDebugSensorRanges(QPainter& painter)
{ {
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, SensorRangeComponent>( m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const SensorRangeComponent& sensor) const PositionComponent& pos, const SensorRangeComponent& sensor)
{ {
@@ -458,7 +458,7 @@ void ArenaView::drawDebugSensorRanges(QPainter& painter)
const QPointF center = worldToWidget(pos.value); const QPointF center = worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles) const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(tilePx()); * static_cast<qreal>(getTilePx());
QColor circleColor = it->second.outline; QColor circleColor = it->second.outline;
circleColor.setAlpha(77); circleColor.setAlpha(77);
painter.setPen(QPen(circleColor, 1)); painter.setPen(QPen(circleColor, 1));
@@ -487,7 +487,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
painter.drawLine(worldToWidget(from), worldToWidget(to)); painter.drawLine(worldToWidget(from), worldToWidget(to));
}; };
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, AttackBehavior>( FactionComponent, AttackBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac, const PositionComponent& pos, const FactionComponent& fac,
@@ -502,7 +502,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
drawTargetLine(fac.isEnemy, pos.value, *targetPos); drawTargetLine(fac.isEnemy, pos.value, *targetPos);
}); });
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, RepairBehavior>( FactionComponent, RepairBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac, const PositionComponent& pos, const FactionComponent& fac,
@@ -517,7 +517,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
drawTargetLine(fac.isEnemy, pos.value, *targetPos); drawTargetLine(fac.isEnemy, pos.value, *targetPos);
}); });
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, SalvageScrapBehavior>( FactionComponent, SalvageScrapBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac, const PositionComponent& pos, const FactionComponent& fac,

View File

@@ -32,7 +32,7 @@ public:
~ArenaView() override; ~ArenaView() override;
void setGameSpeed(double multiplier); void setGameSpeed(double multiplier);
double gameSpeed() const; double getGameSpeed() const;
void togglePause(); void togglePause();
void stopRendering(); void stopRendering();
@@ -56,7 +56,7 @@ private:
void drawDebugTargetLines(QPainter& painter); void drawDebugTargetLines(QPainter& painter);
void drawBeams(QPainter& painter); void drawBeams(QPainter& painter);
float tilePx() const; float getTilePx() const;
QPointF worldToWidget(QVector2D worldPos) const; QPointF worldToWidget(QVector2D worldPos) const;
QPointF tileToWidget(QPoint tile) const; QPointF tileToWidget(QPoint tile) const;
QRectF tileRect(QPoint tile) const; QRectF tileRect(QPoint tile) const;

View File

@@ -141,7 +141,7 @@ void ArenaWidget::updateStatus(const ArenaStatus& status)
} }
threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0))); threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0)));
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.ehpPercentText()))); ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.getEhpPercentText())));
QString lines; QString lines;
for (const ArenaStatus::Entry& entry : team.entries) for (const ArenaStatus::Entry& entry : team.entries)

View File

@@ -46,7 +46,7 @@ namespace
header = QStringLiteral("[WON] ") + header; header = QStringLiteral("[WON] ") + header;
} }
header += QStringLiteral(" - threat %1").arg(QString::number(team.threatLevel, 'f', 0)); header += QStringLiteral(" - threat %1").arg(QString::number(team.threatLevel, 'f', 0));
header += QStringLiteral(" - EHP %1").arg(QString::fromStdString(team.ehpPercentText())); header += QStringLiteral(" - EHP %1").arg(QString::fromStdString(team.getEhpPercentText()));
return escapeCell(header); return escapeCell(header);
} }
@@ -147,7 +147,7 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent); entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
contentLayout->addWidget(entry.widget); contentLayout->addWidget(entry.widget);
entry.widget->updateStatus(entry.simulation->status()); entry.widget->updateStatus(entry.simulation->getStatus());
m_arenas.push_back(std::move(entry)); m_arenas.push_back(std::move(entry));
} }
@@ -179,14 +179,14 @@ void BalancingWindow::pollStatuses()
{ {
if (entry.worker.joinable()) if (entry.worker.joinable())
{ {
const ArenaStatus status = entry.simulation->status(); const ArenaStatus status = entry.simulation->getStatus();
entry.widget->updateStatus(status); entry.widget->updateStatus(status);
} }
} }
if (m_inspectedSim && m_inspectedArenaIndex >= 0) if (m_inspectedSim && m_inspectedArenaIndex >= 0)
{ {
const ArenaStatus status = m_inspectedSim->status(); const ArenaStatus status = m_inspectedSim->getStatus();
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->updateStatus(status); m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->updateStatus(status);
} }
@@ -242,7 +242,7 @@ void BalancingWindow::startArena(int index)
entry.simulation = std::make_unique<ArenaSimulation>( entry.simulation = std::make_unique<ArenaSimulation>(
m_gameConfig, entry.config, m_nextSeed++); m_gameConfig, entry.config, m_nextSeed++);
entry.widget->startSimulation(); entry.widget->startSimulation();
entry.widget->updateStatus(entry.simulation->status()); entry.widget->updateStatus(entry.simulation->getStatus());
ArenaSimulation* sim = entry.simulation.get(); ArenaSimulation* sim = entry.simulation.get();
entry.worker = std::thread([sim]() { sim->run(); }); entry.worker = std::thread([sim]() { sim->run(); });
updateButtons(); updateButtons();
@@ -278,7 +278,7 @@ void BalancingWindow::inspectArena(int index)
entry.widget->resetToGrey(); entry.widget->resetToGrey();
entry.widget->startSimulation(); entry.widget->startSimulation();
entry.widget->updateStatus(m_inspectedSim->status()); entry.widget->updateStatus(m_inspectedSim->getStatus());
m_inspectWindow = new InspectWindow( m_inspectWindow = new InspectWindow(
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr); m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
@@ -336,7 +336,7 @@ void BalancingWindow::updateButtons()
bool allRunning = true; bool allRunning = true;
for (ArenaEntry& entry : m_arenas) for (ArenaEntry& entry : m_arenas)
{ {
if (entry.worker.joinable() && !entry.simulation->status().finished) if (entry.worker.joinable() && !entry.simulation->getStatus().finished)
{ {
anyRunning = true; anyRunning = true;
} }

View File

@@ -199,7 +199,7 @@ void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> eve
void InspectWindow::pollStatus() void InspectWindow::pollStatus()
{ {
const ArenaStatus status = m_sim->status(); const ArenaStatus status = m_sim->getStatus();
updateInfoPanel(status); updateInfoPanel(status);
refreshEntityStats(); refreshEntityStats();
} }
@@ -228,7 +228,7 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
} }
threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0))); threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0)));
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.ehpPercentText()))); ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.getEhpPercentText())));
QString lines; QString lines;
for (const ArenaStatus::Entry& entry : team.entries) for (const ArenaStatus::Entry& entry : team.entries)
@@ -256,7 +256,7 @@ void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event
{ {
m_selectedEntity = event->entity; m_selectedEntity = event->entity;
EntityAdmin& admin = m_sim->admin(); EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity; entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity)) if (!admin.isValid(entity))
@@ -332,7 +332,7 @@ void InspectWindow::refreshEntityStats()
{ {
if (!m_selectedEntity.has_value()) { return; } if (!m_selectedEntity.has_value()) { return; }
EntityAdmin& admin = m_sim->admin(); EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity; entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity)) if (!admin.isValid(entity))

View File

@@ -279,6 +279,12 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.buildingBlocksTooltip = *tip; cfg.buildingBlocksTooltip = *tip;
} }
if (const std::optional<std::string> tip =
tbl["world"]["artifact_tooltip"].value<std::string>())
{
cfg.artifactTooltip = *tip;
}
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles")); cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles")); cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles")); cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));

View File

@@ -30,7 +30,7 @@ public:
// Evaluates the expression at the given x. Requires a compiled formula. // Evaluates the expression at the given x. Requires a compiled formula.
double evaluate(double x) const; double evaluate(double x) const;
const std::string& source() const { return m_source; } const std::string& getSource() const { return m_source; }
bool isValid() const { return m_expr != nullptr; } bool isValid() const { return m_expr != nullptr; }
private: private:

View File

@@ -82,6 +82,10 @@ struct WorldConfig
// (REQ-UI-BLOCKS-TOOLTIP). Presentation-only; the simulation ignores it. // (REQ-UI-BLOCKS-TOOLTIP). Presentation-only; the simulation ignores it.
std::optional<std::string> buildingBlocksTooltip; std::optional<std::string> buildingBlocksTooltip;
// Optional hover-tooltip for the header artifact count display
// (REQ-UI-ARTIFACTS-TOOLTIP). Presentation-only; the simulation ignores it.
std::optional<std::string> artifactTooltip;
WorldRegions regions; WorldRegions regions;
WorldExpansion expansion; WorldExpansion expansion;
WorldPush push; WorldPush push;

View File

@@ -30,7 +30,7 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
// 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, scraps);
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo(); const std::vector<ScrapInfo> allScrap = scraps.getAllScrapInfo();
// Tick down per-module collection cooldowns. // Tick down per-module collection cooldowns.
m_admin.forEach<SalvagerComponent>( m_admin.forEach<SalvagerComponent>(

View File

@@ -65,7 +65,7 @@ bool ScrapSystem::collectOne(entt::entity entity)
return true; return true;
} }
std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const std::vector<ScrapInfo> ScrapSystem::getAllScrapInfo() const
{ {
std::vector<ScrapInfo> result; std::vector<ScrapInfo> result;
m_admin.forEach<ScrapDataComponent>( m_admin.forEach<ScrapDataComponent>(

View File

@@ -35,7 +35,7 @@ public:
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 scrap.
std::vector<ScrapInfo> allScrapInfo() const; std::vector<ScrapInfo> getAllScrapInfo() const;
private: private:
EntityAdmin& m_admin; EntityAdmin& m_admin;

View File

@@ -19,7 +19,7 @@ void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const ScrapSystem& scra
{ {
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.allScrapInfo(); const std::vector<ScrapInfo> allScrap = scraps.getAllScrapInfo();
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,

View File

@@ -86,7 +86,7 @@ struct Building
// Total items held on the output side: buffered plus still-emerging. The // Total items held on the output side: buffered plus still-emerging. The
// output-buffer capacity rule (REQ-MAT-OUTPUT-BUFFER) counts emerging items, // output-buffer capacity rule (REQ-MAT-OUTPUT-BUFFER) counts emerging items,
// since they have not yet left the building. // since they have not yet left the building.
int outputItemCount() const int getOutputItemCount() const
{ {
int count = static_cast<int>(outputBuffer.items.size()); int count = static_cast<int>(outputBuffer.items.size());
for (const std::vector<BeltItemSlot>& lane : emergingItems) for (const std::vector<BeltItemSlot>& lane : emergingItems)

View File

@@ -26,15 +26,15 @@ struct SelectedBuilding
// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE). // (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE).
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id) std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
{ {
const Building* building = sim.buildings().findBuilding(id); const Building* building = sim.getBuildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.buildings().findSite(id); const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
if (!building && !site) if (!building && !site)
{ {
return std::nullopt; return std::nullopt;
} }
const BuildingType type = building ? building->type : site->type; const BuildingType type = building ? building->type : site->type;
const BuildingDef* def = sim.config().buildings.findBuildingDef(type); const BuildingDef* def = sim.getConfig().buildings.findBuildingDef(type);
if (!def || !def->playerPlaceable) if (!def || !def->playerPlaceable)
{ {
return std::nullopt; return std::nullopt;
@@ -52,8 +52,8 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id) std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
{ {
const Building* building = sim.buildings().findBuilding(id); const Building* building = sim.getBuildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.buildings().findSite(id); const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
if (!building && !site) if (!building && !site)
{ {
return std::nullopt; return std::nullopt;
@@ -77,7 +77,7 @@ std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, Building
{ {
// Operational splitter filters live in the BeltSystem, keyed by tile. // Operational splitter filters live in the BeltSystem, keyed by tile.
const std::optional<BeltSystem::SplitterInfo> info = const std::optional<BeltSystem::SplitterInfo> info =
sim.belts().getSplitterInfo(building->anchor); sim.getBelts().getSplitterInfo(building->anchor);
if (info.has_value()) if (info.has_value())
{ {
config.splitterFilterA = info->filterA; config.splitterFilterA = info->filterA;

View File

@@ -1014,7 +1014,7 @@ void BuildingSystem::tickProduction(Tick currentTick)
// 3. Output buffer has space for chosen outputs? Emerging items still // 3. Output buffer has space for chosen outputs? Emerging items still
// count against the buffer (REQ-MAT-OUTPUT-EMERGE). // count against the buffer (REQ-MAT-OUTPUT-EMERGE).
const int newSize = building.outputItemCount() const int newSize = building.getOutputItemCount()
+ static_cast<int>(chosen.size()); + static_cast<int>(chosen.size());
if (newSize > building.outputBuffer.capacity) if (newSize > building.outputBuffer.capacity)
{ {
@@ -1279,12 +1279,12 @@ const ConstructionSite* BuildingSystem::findSite(BuildingId id) const
return nullptr; return nullptr;
} }
std::vector<Building> BuildingSystem::allBuildings() const std::vector<Building> BuildingSystem::getAllBuildings() const
{ {
return m_buildings; return m_buildings;
} }
std::vector<ConstructionSite> BuildingSystem::allSites() const std::vector<ConstructionSite> BuildingSystem::getAllSites() const
{ {
return std::vector<ConstructionSite>(m_constructionQueue.begin(), return std::vector<ConstructionSite>(m_constructionQueue.begin(),
m_constructionQueue.end()); m_constructionQueue.end());
@@ -1308,7 +1308,7 @@ bool isProductionBuildingType(BuildingType type)
} }
} // namespace } // namespace
int BuildingSystem::productionBuildingCount() const int BuildingSystem::getProductionBuildingCount() const
{ {
int count = 0; int count = 0;
for (const Building& b : m_buildings) for (const Building& b : m_buildings)
@@ -1318,7 +1318,7 @@ int BuildingSystem::productionBuildingCount() const
return count; return count;
} }
int BuildingSystem::activeProductionBuildingCount() const int BuildingSystem::getActiveProductionBuildingCount() const
{ {
int count = 0; int count = 0;
for (const Building& b : m_buildings) for (const Building& b : m_buildings)
@@ -1328,7 +1328,7 @@ int BuildingSystem::activeProductionBuildingCount() const
return count; return count;
} }
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::getAllBeltTiles() const
{ {
std::vector<BeltTileInfo> result; std::vector<BeltTileInfo> result;
for (const Building& b : m_buildings) for (const Building& b : m_buildings)
@@ -1518,7 +1518,7 @@ bool BuildingSystem::deliverScrapToSalvageBay(BuildingId bayId)
} }
// Emerging scrap still counts against the bay's holding capacity // Emerging scrap still counts against the bay's holding capacity
// (REQ-MAT-OUTPUT-EMERGE). // (REQ-MAT-OUTPUT-EMERGE).
if (bay->outputItemCount() >= bay->outputBuffer.capacity) if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
{ {
return false; return false;
} }

View File

@@ -112,17 +112,17 @@ public:
const Building* findBuilding(BuildingId id) const; const Building* findBuilding(BuildingId id) const;
const ConstructionSite* findSite(BuildingId id) const; const ConstructionSite* findSite(BuildingId id) const;
std::vector<Building> allBuildings() const; std::vector<Building> getAllBuildings() const;
std::vector<ConstructionSite> allSites() const; std::vector<ConstructionSite> getAllSites() const;
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed // REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings. // (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
int productionBuildingCount() const; int getProductionBuildingCount() const;
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above // REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
// that currently has an active production cycle. // that currently has an active production cycle.
int activeProductionBuildingCount() const; int getActiveProductionBuildingCount() const;
std::vector<BeltTileInfo> allBeltTiles() const; std::vector<BeltTileInfo> getAllBeltTiles() const;
bool isTileOccupied(QPoint tile) const; bool isTileOccupied(QPoint tile) const;
// Visits every item currently emerging from a building output port on its // Visits every item currently emerging from a building output port on its

View File

@@ -46,18 +46,18 @@ void CommandManager::drain()
{ {
// Restart is a file boundary: a fresh file with the new seed. // Restart is a file boundary: a fresh file with the new seed.
m_recorder->startNewRun(m_simulation.getSeed(), m_recorder->startNewRun(m_simulation.getSeed(),
m_simulation.rngFingerprint()); m_simulation.getRngFingerprint());
} }
} }
else else
{ {
// Commands drain before the tick batch, so currentTick is the count of // Commands drain before the tick batch, so currentTick is the count of
// completed ticks the command is pinned to. // completed ticks the command is pinned to.
const Tick tick = m_simulation.currentTick(); const Tick tick = m_simulation.getCurrentTick();
m_simulation.apply(*command); m_simulation.apply(*command);
if (m_recorder) if (m_recorder)
{ {
m_recorder->recordCommand(tick, *command, m_simulation.rngFingerprint()); m_recorder->recordCommand(tick, *command, m_simulation.getRngFingerprint());
} }
} }
} }
@@ -74,7 +74,7 @@ void CommandManager::setRecorder(std::unique_ptr<ReplayRecorder> recorder)
m_recorder = std::move(recorder); m_recorder = std::move(recorder);
if (m_recorder) if (m_recorder)
{ {
m_recorder->startNewRun(m_simulation.getSeed(), m_simulation.rngFingerprint()); m_recorder->startNewRun(m_simulation.getSeed(), m_simulation.getRngFingerprint());
} }
} }
@@ -85,8 +85,8 @@ void CommandManager::setReplayMode(bool replayMode)
void CommandManager::recordTickCheckpoint() void CommandManager::recordTickCheckpoint()
{ {
if (m_recorder && (m_simulation.currentTick() % kChecksumIntervalTicks == 0)) if (m_recorder && (m_simulation.getCurrentTick() % kChecksumIntervalTicks == 0))
{ {
m_recorder->recordChecksum(m_simulation.currentTick(), m_simulation.rngFingerprint()); m_recorder->recordChecksum(m_simulation.getCurrentTick(), m_simulation.getRngFingerprint());
} }
} }

View File

@@ -48,7 +48,7 @@ void ReplayPlayer::processEntriesAt(Tick tick)
{ {
m_simulation.apply(*entry.command); m_simulation.apply(*entry.command);
} }
else if (m_simulation.rngFingerprint() != entry.fingerprint) else if (m_simulation.getRngFingerprint() != entry.fingerprint)
{ {
m_desyncTick = tick; m_desyncTick = tick;
} }

View File

@@ -19,7 +19,7 @@ class Simulation;
// each frame, for each tick to run: // each frame, for each tick to run:
// if (player.isFinished()) break; // if (player.isFinished()) break;
// sim.tick(); // sim.tick();
// player.advanceTo(sim.currentTick()); // player.advanceTo(sim.getCurrentTick());
class ReplayPlayer class ReplayPlayer
{ {
public: public:

View File

@@ -58,7 +58,7 @@ std::string computeReplayConfigHash(const std::string& configDir)
hasher.appendBytes(bytes.constData(), static_cast<std::size_t>(bytes.size())); hasher.appendBytes(bytes.constData(), static_cast<std::size_t>(bytes.size()));
} }
} }
return toHex(hasher.value()); return toHex(hasher.getValue());
} }
void ReplayRecorder::startNewRun(unsigned int seed, std::uint64_t initialRngFingerprint) void ReplayRecorder::startNewRun(unsigned int seed, std::uint64_t initialRngFingerprint)
@@ -124,7 +124,7 @@ bool ReplayRecorder::isOpen() const
return m_stream.is_open(); return m_stream.is_open();
} }
const std::string& ReplayRecorder::currentFilePath() const const std::string& ReplayRecorder::getCurrentFilePath() const
{ {
return m_filePath; return m_filePath;
} }

View File

@@ -44,7 +44,7 @@ public:
void close(); void close();
bool isOpen() const; bool isOpen() const;
const std::string& currentFilePath() const; const std::string& getCurrentFilePath() const;
private: private:
std::string m_configDir; std::string m_configDir;

View File

@@ -113,7 +113,7 @@ Simulation::~Simulation()
unregisterForEvents(); unregisterForEvents();
} }
const GameConfig& Simulation::config() const const GameConfig& Simulation::getConfig() const
{ {
return m_config; return m_config;
} }
@@ -579,7 +579,7 @@ void Simulation::tickDeathsAndLoot()
} }
else else
{ {
const double genD = static_cast<double>(m_waveSystem->generation()); const double genD = static_cast<double>(m_waveSystem->getGeneration());
scrap = static_cast<int>( scrap = static_cast<int>(
m_config.stations.enemyStation.scrapDropFormula.evaluate(genD)); m_config.stations.enemyStation.scrapDropFormula.evaluate(genD));
} }
@@ -619,9 +619,9 @@ void Simulation::tickDeathsAndLoot()
if (es0Gone && es1Gone && if (es0Gone && es1Gone &&
m_currentEnemyStationEntities[0] != entt::null) m_currentEnemyStationEntities[0] != entt::null)
{ {
const int destroyedLevel = m_waveSystem->generation(); const int destroyedLevel = m_waveSystem->getGeneration();
m_waveSystem->onEnemyStationsDestroyed(); m_waveSystem->onEnemyStationsDestroyed();
placeEnemyStationSet(m_waveSystem->generation()); placeEnemyStationSet(m_waveSystem->getGeneration());
generateSchematicChoices(destroyedLevel); generateSchematicChoices(destroyedLevel);
} }
} }
@@ -968,7 +968,7 @@ void Simulation::appendStringSet(Hasher& hasher, const std::set<std::string>& id
} }
} }
unsigned long long Simulation::rngFingerprint() const unsigned long long Simulation::getRngFingerprint() const
{ {
return fingerprintRng(m_rng); return fingerprintRng(m_rng);
} }
@@ -991,11 +991,11 @@ unsigned long long Simulation::computeStateChecksum() const
hasher.append(m_expansionsPurchased); hasher.append(m_expansionsPurchased);
// WaveSystem scalar state, reached through existing accessors. // WaveSystem scalar state, reached through existing accessors.
hasher.append(threatLevel()); hasher.append(getThreatLevel());
hasher.append(threatAccumulationRate()); hasher.append(getThreatAccumulationRate());
hasher.append(bossWaveCounter()); hasher.append(getBossWaveCounter());
hasher.append(bossCountdownTicks()); hasher.append(getBossCountdownTicks());
hasher.append(normalGapRemainingTicks()); hasher.append(getNormalGapRemainingTicks());
// Schematic / unlock state (std::map and std::set iterate in sorted order). // Schematic / unlock state (std::map and std::set iterate in sorted order).
appendSchematicMap(hasher, m_schematicLevels); appendSchematicMap(hasher, m_schematicLevels);
@@ -1052,7 +1052,7 @@ unsigned long long Simulation::computeStateChecksum() const
hasher.append(c.schematicId); hasher.append(c.schematicId);
}); });
return hasher.value(); return hasher.getValue();
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1081,7 +1081,7 @@ bool Simulation::hasSchematicChoicesPending() const
// Accessors // Accessors
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Tick Simulation::currentTick() const Tick Simulation::getCurrentTick() const
{ {
return m_currentTick; return m_currentTick;
} }
@@ -1091,18 +1091,18 @@ unsigned int Simulation::getSeed() const
return m_seed; return m_seed;
} }
int Simulation::buildingBlocksStock() const int Simulation::getBuildingBlocksStock() const
{ {
return m_buildingBlocksStock; return m_buildingBlocksStock;
} }
int Simulation::currentAsteroidWidth_tiles() const int Simulation::getCurrentAsteroidWidth_tiles() const
{ {
return m_config.world.regions.asteroidWidth_tiles return m_config.world.regions.asteroidWidth_tiles
+ m_expansionsPurchased * m_config.world.expansion.columnsPerExpansion_tiles; + m_expansionsPurchased * m_config.world.expansion.columnsPerExpansion_tiles;
} }
int Simulation::currentExpansionCost() const int Simulation::getCurrentExpansionCost() const
{ {
const double cost = m_config.world.expansion.costBuildingBlocksFormula.evaluate( const double cost = m_config.world.expansion.costBuildingBlocksFormula.evaluate(
static_cast<double>(m_expansionsPurchased)); static_cast<double>(m_expansionsPurchased));
@@ -1111,14 +1111,14 @@ int Simulation::currentExpansionCost() const
void Simulation::tryExpandAsteroid() void Simulation::tryExpandAsteroid()
{ {
const int cost = currentExpansionCost(); const int cost = getCurrentExpansionCost();
if (m_buildingBlocksStock < cost) if (m_buildingBlocksStock < cost)
{ {
return; return;
} }
m_buildingBlocksStock -= cost; m_buildingBlocksStock -= cost;
++m_expansionsPurchased; ++m_expansionsPurchased;
m_buildingSystem->setAsteroidWidth_tiles(currentAsteroidWidth_tiles()); m_buildingSystem->setAsteroidWidth_tiles(getCurrentAsteroidWidth_tiles());
} }
bool Simulation::isGameOver() const bool Simulation::isGameOver() const
@@ -1131,44 +1131,44 @@ bool Simulation::isWon() const
return m_isWon; return m_isWon;
} }
int Simulation::artifactCount() const int Simulation::getArtifactCount() const
{ {
return m_artifactCount; return m_artifactCount;
} }
double Simulation::threatLevel() const double Simulation::getThreatLevel() const
{ {
return m_waveSystem->threatLevel(); return m_waveSystem->getThreatLevel();
} }
double Simulation::threatAccumulationRate() const double Simulation::getThreatAccumulationRate() const
{ {
return m_waveSystem->threatAccumulationRate(); return m_waveSystem->getThreatAccumulationRate();
} }
double Simulation::maxFactoryProductionThreatRate() const double Simulation::getMaxFactoryProductionThreatRate() const
{ {
return static_cast<double>(m_buildingSystem->productionBuildingCount()); return static_cast<double>(m_buildingSystem->getProductionBuildingCount());
} }
double Simulation::currentFactoryProductionThreatRate() const double Simulation::getCurrentFactoryProductionThreatRate() const
{ {
return static_cast<double>(m_buildingSystem->activeProductionBuildingCount()); return static_cast<double>(m_buildingSystem->getActiveProductionBuildingCount());
} }
int Simulation::bossWaveCounter() const int Simulation::getBossWaveCounter() const
{ {
return m_waveSystem->bossWaveCounter(); return m_waveSystem->getBossWaveCounter();
} }
Tick Simulation::bossCountdownTicks() const Tick Simulation::getBossCountdownTicks() const
{ {
return m_waveSystem->bossCountdownTicks(); return m_waveSystem->getBossCountdownTicks();
} }
Tick Simulation::normalGapRemainingTicks() const Tick Simulation::getNormalGapRemainingTicks() const
{ {
return m_waveSystem->normalGapRemainingTicks(); return m_waveSystem->getNormalGapRemainingTicks();
} }
bool Simulation::isSchematicUnlocked(const std::string& shipId) const bool Simulation::isSchematicUnlocked(const std::string& shipId) const
@@ -1222,52 +1222,52 @@ void Simulation::demolish(BuildingId id)
m_buildingBlocksStock += m_buildingSystem->demolish(id); m_buildingBlocksStock += m_buildingSystem->demolish(id);
} }
BuildingSystem& Simulation::buildingsMutable() BuildingSystem& Simulation::getBuildingsMutable()
{ {
return *m_buildingSystem; return *m_buildingSystem;
} }
const BuildingSystem& Simulation::buildings() const const BuildingSystem& Simulation::getBuildings() const
{ {
return *m_buildingSystem; return *m_buildingSystem;
} }
BeltSystem& Simulation::beltsMutable() BeltSystem& Simulation::getBeltsMutable()
{ {
return m_beltSystem; return m_beltSystem;
} }
const BeltSystem& Simulation::belts() const const BeltSystem& Simulation::getBelts() const
{ {
return m_beltSystem; return m_beltSystem;
} }
ShipSystem& Simulation::ships() ShipSystem& Simulation::getShips()
{ {
return *m_shipSystem; return *m_shipSystem;
} }
const ShipSystem& Simulation::ships() const const ShipSystem& Simulation::getShips() const
{ {
return *m_shipSystem; return *m_shipSystem;
} }
ScrapSystem& Simulation::scraps() ScrapSystem& Simulation::getScraps()
{ {
return *m_scrapSystem; return *m_scrapSystem;
} }
const ScrapSystem& Simulation::scraps() const const ScrapSystem& Simulation::getScraps() const
{ {
return *m_scrapSystem; return *m_scrapSystem;
} }
EntityAdmin& Simulation::admin() EntityAdmin& Simulation::getAdmin()
{ {
return m_admin; return m_admin;
} }
const EntityAdmin& Simulation::admin() const const EntityAdmin& Simulation::getAdmin() const
{ {
return m_admin; return m_admin;
} }

View File

@@ -41,7 +41,7 @@ public:
explicit Simulation(GameConfig config, unsigned int seed = 0); explicit Simulation(GameConfig config, unsigned int seed = 0);
~Simulation(); ~Simulation();
const GameConfig& config() const; const GameConfig& getConfig() const;
// Reinitializes all simulation state as if constructed fresh. // Reinitializes all simulation state as if constructed fresh.
void reset(unsigned int seed = 0); void reset(unsigned int seed = 0);
@@ -68,26 +68,26 @@ public:
// Returns true if there are pending schematic choices waiting for player input. // Returns true if there are pending schematic choices waiting for player input.
bool hasSchematicChoicesPending() const; bool hasSchematicChoicesPending() const;
Tick currentTick() const; Tick getCurrentTick() const;
// The seed this run was (re)initialized with; written to the replay header. // The seed this run was (re)initialized with; written to the replay header.
unsigned int getSeed() const; unsigned int getSeed() const;
int buildingBlocksStock() const; int getBuildingBlocksStock() const;
// Current asteroid width in tiles = base width + purchased expansions // Current asteroid width in tiles = base width + purchased expansions
// (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND). // (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND).
int currentAsteroidWidth_tiles() const; int getCurrentAsteroidWidth_tiles() const;
// Building block cost of the next expansion, floored to an integer // Building block cost of the next expansion, floored to an integer
// (REQ-EXP-COST); x = number of expansions already purchased. // (REQ-EXP-COST); x = number of expansions already purchased.
int currentExpansionCost() const; int getCurrentExpansionCost() const;
bool isGameOver() const; bool isGameOver() const;
bool isWon() const; bool isWon() const;
int artifactCount() const; int getArtifactCount() const;
double threatLevel() const; double getThreatLevel() const;
double threatAccumulationRate() const; double getThreatAccumulationRate() const;
double maxFactoryProductionThreatRate() const; double getMaxFactoryProductionThreatRate() const;
double currentFactoryProductionThreatRate() const; double getCurrentFactoryProductionThreatRate() const;
int bossWaveCounter() const; int getBossWaveCounter() const;
Tick bossCountdownTicks() const; Tick getBossCountdownTicks() const;
Tick normalGapRemainingTicks() const; Tick getNormalGapRemainingTicks() const;
// Ship schematic state query. // Ship schematic state query.
bool isSchematicUnlocked(const std::string& shipId) const; bool isSchematicUnlocked(const std::string& shipId) const;
@@ -102,25 +102,25 @@ public:
// -- Determinism (see docs/replay_design.md) ----------------------------- // -- Determinism (see docs/replay_design.md) -----------------------------
// 64-bit fingerprint of the RNG stream state. Cheap; written to the replay // 64-bit fingerprint of the RNG stream state. Cheap; written to the replay
// file periodically + after each command for desync detection. // file periodically + after each command for desync detection.
unsigned long long rngFingerprint() const; unsigned long long getRngFingerprint() const;
// 64-bit fingerprint of the full simulation state (RNG, scalars, buildings, // 64-bit fingerprint of the full simulation state (RNG, scalars, buildings,
// belts, and ECS component state). Used by the double-run determinism test; // belts, and ECS component state). Used by the double-run determinism test;
// a superset of rngFingerprint(). // a superset of getRngFingerprint().
unsigned long long computeStateChecksum() const; unsigned long long computeStateChecksum() const;
// Const subsystem accessors (queries only). The mutable counterparts are // Const subsystem accessors (queries only). The mutable counterparts are
// private and reachable only through Simulation::apply (the command // private and reachable only through Simulation::apply (the command
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot // chokepoint) or, in tests, SimulationTestAccess — so production code cannot
// mutate the factory outside the recorded command path (docs/replay_design.md). // mutate the factory outside the recorded command path (docs/replay_design.md).
const BuildingSystem& buildings() const; const BuildingSystem& getBuildings() const;
const BeltSystem& belts() const; const BeltSystem& getBelts() const;
ShipSystem& ships(); ShipSystem& getShips();
const ShipSystem& ships() const; const ShipSystem& getShips() const;
ScrapSystem& scraps(); ScrapSystem& getScraps();
const ScrapSystem& scraps() const; const ScrapSystem& getScraps() const;
EntityAdmin& admin(); EntityAdmin& getAdmin();
const EntityAdmin& admin() const; const EntityAdmin& getAdmin() const;
private: private:
// Grants tests access to the private player-action mutators below without // Grants tests access to the private player-action mutators below without
@@ -148,8 +148,8 @@ private:
void tryExpandAsteroid(); void tryExpandAsteroid();
// Mutable subsystem accessors; same chokepoint rule as the mutators above. // Mutable subsystem accessors; same chokepoint rule as the mutators above.
BuildingSystem& buildingsMutable(); BuildingSystem& getBuildingsMutable();
BeltSystem& beltsMutable(); BeltSystem& getBeltsMutable();
void handleEvent(std::shared_ptr<const TracePrintRequestedEvent> event) override; void handleEvent(std::shared_ptr<const TracePrintRequestedEvent> event) override;

View File

@@ -57,5 +57,5 @@ std::uint64_t fingerprintRng(const std::mt19937& rng)
stream << rng; // full internal state as space-separated integers stream << rng; // full internal state as space-separated integers
Hasher hasher; Hasher hasher;
hasher.append(stream.str()); hasher.append(stream.str());
return hasher.value(); return hasher.getValue();
} }

View File

@@ -43,7 +43,7 @@ public:
void append(const QVector2D& vector); void append(const QVector2D& vector);
void append(const std::string& text); void append(const std::string& text);
std::uint64_t value() const { return m_state; } std::uint64_t getValue() const { return m_state; }
private: private:
std::uint64_t m_state = 14695981039346656037ull; // FNV-1a 64-bit offset basis std::uint64_t m_state = 14695981039346656037ull; // FNV-1a 64-bit offset basis

View File

@@ -98,12 +98,12 @@ void WaveSystem::onEnemyStationsDestroyed()
++m_generation; ++m_generation;
} }
double WaveSystem::threatLevel() const double WaveSystem::getThreatLevel() const
{ {
return m_threatLevel; return m_threatLevel;
} }
double WaveSystem::threatAccumulationRate() const double WaveSystem::getThreatAccumulationRate() const
{ {
if (isInQuietWindow()) if (isInQuietWindow())
{ {
@@ -113,22 +113,22 @@ double WaveSystem::threatAccumulationRate() const
return std::max(0.0, m_config.world.waves.threatRateFormula.evaluate(x)); return std::max(0.0, m_config.world.waves.threatRateFormula.evaluate(x));
} }
int WaveSystem::generation() const int WaveSystem::getGeneration() const
{ {
return m_generation; return m_generation;
} }
int WaveSystem::bossWaveCounter() const int WaveSystem::getBossWaveCounter() const
{ {
return m_bossWaveCounter; return m_bossWaveCounter;
} }
Tick WaveSystem::bossCountdownTicks() const Tick WaveSystem::getBossCountdownTicks() const
{ {
return m_bossCountdownTicks; return m_bossCountdownTicks;
} }
Tick WaveSystem::normalGapRemainingTicks() const Tick WaveSystem::getNormalGapRemainingTicks() const
{ {
return m_normalGapRemainingTicks; return m_normalGapRemainingTicks;
} }

View File

@@ -34,26 +34,26 @@ public:
// (REQ-WAV-BOSS-ADVANCE, REQ-PSH-STATION-STATS). // (REQ-WAV-BOSS-ADVANCE, REQ-PSH-STATION-STATS).
void onEnemyStationsDestroyed(); void onEnemyStationsDestroyed();
double threatLevel() const; double getThreatLevel() const;
// Current rate at which threatLevel() is increasing, in threat/second // Current rate at which getThreatLevel() is increasing, in threat/second
// (REQ-WAV-THREAT-RATE). 0 during a quiet window (REQ-WAV-QUIET) or when // (REQ-WAV-THREAT-RATE). 0 during a quiet window (REQ-WAV-QUIET) or when
// the rate formula evaluates to a negative value. // the rate formula evaluates to a negative value.
double threatAccumulationRate() const; double getThreatAccumulationRate() const;
// Current enemy-station generation level (0 for initial set, // Current enemy-station generation level (0 for initial set,
// incremented by 1 after each push — REQ-PSH-STATION-STATS). // incremented by 1 after each push — REQ-PSH-STATION-STATS).
int generation() const; int getGeneration() const;
// Boss wave counter (REQ-WAV-BOSS-COUNTER): current cycle number, starts at 1. // Boss wave counter (REQ-WAV-BOSS-COUNTER): current cycle number, starts at 1.
int bossWaveCounter() const; int getBossWaveCounter() const;
// Ticks remaining until the next boss wave fires (REQ-WAV-BOSS-COUNTDOWN). // Ticks remaining until the next boss wave fires (REQ-WAV-BOSS-COUNTDOWN).
Tick bossCountdownTicks() const; Tick getBossCountdownTicks() const;
// Ticks remaining on the current normal-wave gap timer (REQ-WAV-GAP). // Ticks remaining on the current normal-wave gap timer (REQ-WAV-GAP).
// Frozen during quiet windows. // Frozen during quiet windows.
Tick normalGapRemainingTicks() const; Tick getNormalGapRemainingTicks() const;
private: private:
struct SpawnEntry struct SpawnEntry

View File

@@ -19,7 +19,7 @@ static GameConfig loadConfig()
static void killEnemyStations(Simulation& sim) static void killEnemyStations(Simulation& sim)
{ {
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health) [](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health)
{ {
if (faction.isEnemy) if (faction.isEnemy)
@@ -65,7 +65,7 @@ TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game
"[artifact_win]") "[artifact_win]")
{ {
const Simulation sim(loadConfig()); const Simulation sim(loadConfig());
CHECK(sim.artifactCount() == 0); CHECK(sim.getArtifactCount() == 0);
CHECK_FALSE(sim.isWon()); CHECK_FALSE(sim.isWon());
} }
@@ -145,7 +145,7 @@ TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
SimulationTestAccess::applySchematicChoice(sim,index); SimulationTestAccess::applySchematicChoice(sim,index);
CHECK(sim.artifactCount() == 1); CHECK(sim.getArtifactCount() == 1);
} }
TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count", TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count",
@@ -166,7 +166,7 @@ TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increm
SimulationTestAccess::applySchematicChoice(sim,static_cast<int>(it - choices.begin())); SimulationTestAccess::applySchematicChoice(sim,static_cast<int>(it - choices.begin()));
CHECK(sim.artifactCount() == 0); CHECK(sim.getArtifactCount() == 0);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -205,7 +205,7 @@ TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below
REQUIRE(sim.hasSchematicChoicesPending()); REQUIRE(sim.hasSchematicChoicesPending());
SimulationTestAccess::applySchematicChoice(sim,findArtifactChoiceIndex(sim)); SimulationTestAccess::applySchematicChoice(sim,findArtifactChoiceIndex(sim));
CHECK(sim.artifactCount() == 1); CHECK(sim.getArtifactCount() == 1);
CHECK_FALSE(sim.isWon()); CHECK_FALSE(sim.isWon());
} }
@@ -226,7 +226,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required nu
SimulationTestAccess::applySchematicChoice(sim,index); SimulationTestAccess::applySchematicChoice(sim,index);
} }
CHECK(sim.artifactCount() == 2); CHECK(sim.getArtifactCount() == 2);
CHECK(sim.isWon()); CHECK(sim.isWon());
} }
@@ -249,6 +249,6 @@ TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state",
sim.reset(); sim.reset();
CHECK(sim.artifactCount() == 0); CHECK(sim.getArtifactCount() == 0);
CHECK_FALSE(sim.isWon()); CHECK_FALSE(sim.isWon());
} }

View File

@@ -533,9 +533,9 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
REQUIRE(idA != kInvalidBuildingId); REQUIRE(idA != kInvalidBuildingId);
REQUIRE(idB != kInvalidBuildingId); REQUIRE(idB != kInvalidBuildingId);
REQUIRE(sim.buildings().isTileOccupied(cursor + offsetA)); // (-6, 0) REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
REQUIRE(sim.buildings().isTileOccupied(cursor + offsetB)); // (-4, 0) REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
REQUIRE_FALSE(sim.buildings().isTileOccupied(cursor)); // center not occupied REQUIRE_FALSE(sim.getBuildings().isTileOccupied(cursor)); // center not occupied
} }
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]") TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
@@ -544,20 +544,20 @@ TEST_CASE("Blueprint placement: cost is deducted for each building in sequence",
// Find belt cost from config (belt cost = 2 in test config). // Find belt cost from config (belt cost = 2 in test config).
int beltCost = 0; int beltCost = 0;
for (const BuildingDef& def : sim.config().buildings.buildings) for (const BuildingDef& def : sim.getConfig().buildings.buildings)
{ {
if (def.type == BuildingType::Belt) { beltCost = def.cost; break; } if (def.type == BuildingType::Belt) { beltCost = def.cost; break; }
} }
REQUIRE(beltCost > 0); REQUIRE(beltCost > 0);
const int startBlocks = sim.buildingBlocksStock(); const int startBlocks = sim.getBuildingBlocksStock();
REQUIRE(startBlocks >= 2 * beltCost); // test config has enough starting blocks REQUIRE(startBlocks >= 2 * beltCost); // test config has enough starting blocks
SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-6, 0), Rotation::East); SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-6, 0), Rotation::East);
REQUIRE(sim.buildingBlocksStock() == startBlocks - beltCost); REQUIRE(sim.getBuildingBlocksStock() == startBlocks - beltCost);
SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-4, 0), Rotation::East); SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-4, 0), Rotation::East);
REQUIRE(sim.buildingBlocksStock() == startBlocks - 2 * beltCost); REQUIRE(sim.getBuildingBlocksStock() == startBlocks - 2 * beltCost);
} }
TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing", TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing",
@@ -567,7 +567,7 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a
// Find miner cost (15 in test config) — expensive enough to exhaust a small stock. // Find miner cost (15 in test config) — expensive enough to exhaust a small stock.
int minerCost = 0; int minerCost = 0;
for (const BuildingDef& def : sim.config().buildings.buildings) for (const BuildingDef& def : sim.getConfig().buildings.buildings)
{ {
if (def.type == BuildingType::Miner) { minerCost = def.cost; break; } if (def.type == BuildingType::Miner) { minerCost = def.cost; break; }
} }
@@ -576,26 +576,26 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a
// Drain the stock by placing miners until we no longer have enough. // Drain the stock by placing miners until we no longer have enough.
// Non-overlapping columns: miner body is 2 wide, so step by 2. // Non-overlapping columns: miner body is 2 wide, so step by 2.
int col = -2; int col = -2;
while (sim.buildingBlocksStock() >= minerCost) while (sim.getBuildingBlocksStock() >= minerCost)
{ {
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(col, 0), Rotation::East); SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(col, 0), Rotation::East);
col -= 2; col -= 2;
} }
const int blocksBeforeAttempt = sim.buildingBlocksStock(); const int blocksBeforeAttempt = sim.getBuildingBlocksStock();
const BuildingId id = SimulationTestAccess::place(sim, const BuildingId id = SimulationTestAccess::place(sim,
BuildingType::Miner, QPoint(col - 2, 0), Rotation::East); BuildingType::Miner, QPoint(col - 2, 0), Rotation::East);
// Placement must fail and leave the stock unchanged. // Placement must fail and leave the stock unchanged.
REQUIRE(id == kInvalidBuildingId); REQUIRE(id == kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == blocksBeforeAttempt); REQUIRE(sim.getBuildingBlocksStock() == blocksBeforeAttempt);
} }
TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing", TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing",
"[blueprint]") "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadConfig());
const int startBlocks = sim.buildingBlocksStock(); const int startBlocks = sim.getBuildingBlocksStock();
// A miner is all-asteroid; placing it in space (x >= 0) violates the terrain // A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
// rule, so it must be rejected without consuming building blocks. // rule, so it must be rejected without consuming building blocks.
@@ -603,8 +603,8 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(0, 0), Rotation::East); SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(0, 0), Rotation::East);
REQUIRE(id == kInvalidBuildingId); REQUIRE(id == kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == startBlocks); REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
REQUIRE(sim.buildings().allSites().empty()); REQUIRE(sim.getBuildings().getAllSites().empty());
} }
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost", TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
@@ -613,12 +613,12 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies
Simulation sim(loadConfig()); Simulation sim(loadConfig());
int minerCost = 0; int minerCost = 0;
for (const BuildingDef& def : sim.config().buildings.buildings) for (const BuildingDef& def : sim.getConfig().buildings.buildings)
{ {
if (def.type == BuildingType::Miner) { minerCost = def.cost; break; } if (def.type == BuildingType::Miner) { minerCost = def.cost; break; }
} }
REQUIRE(minerCost > 0); REQUIRE(minerCost > 0);
const int startBlocks = sim.buildingBlocksStock(); const int startBlocks = sim.getBuildingBlocksStock();
// Miner mask ["AA","A>"] East at (-3,0) → all-asteroid body at // Miner mask ["AA","A>"] East at (-3,0) → all-asteroid body at
// (-3,0),(-2,0),(-3,1); a valid spot. // (-3,0),(-2,0),(-3,1); a valid spot.
@@ -626,12 +626,12 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-3, 0), Rotation::East); SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-3, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == startBlocks - minerCost); REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
REQUIRE(sim.buildings().isTileOccupied(QPoint(-3, 0))); REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 0)));
REQUIRE(sim.buildings().isTileOccupied(QPoint(-2, 0))); REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-2, 0)));
REQUIRE(sim.buildings().isTileOccupied(QPoint(-3, 1))); REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 1)));
// The output-port tile (1,1)+anchor = (-2,1) is not a body cell. // The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
REQUIRE_FALSE(sim.buildings().isTileOccupied(QPoint(-2, 1))); REQUIRE_FALSE(sim.getBuildings().isTileOccupied(QPoint(-2, 1)));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -671,7 +671,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const ConstructionSite* site = sim.buildings().findSite(id); const ConstructionSite* site = sim.getBuildings().findSite(id);
REQUIRE(site != nullptr); REQUIRE(site != nullptr);
REQUIRE(site->recipeId == "mine_iron_ore"); REQUIRE(site->recipeId == "mine_iron_ore");
} }
@@ -692,7 +692,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
sim.tick(); sim.tick();
} }
const Building* b = sim.buildings().findBuilding(id); const Building* b = sim.getBuildings().findBuilding(id);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
REQUIRE(b->recipeId == "mine_copper_ore"); REQUIRE(b->recipeId == "mine_copper_ore");
} }
@@ -712,8 +712,8 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
const BuildingId id = const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East); SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildings().findSite(id) != nullptr); REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(sim.buildings().findBuilding(id) == nullptr); REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { id }); const Blueprint bp = captureBlueprintFromSelection(sim, { id });
@@ -748,14 +748,14 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
REQUIRE(idA != kInvalidBuildingId); REQUIRE(idA != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); } for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
REQUIRE(sim.buildings().findBuilding(idA) != nullptr); REQUIRE(sim.getBuildings().findBuilding(idA) != nullptr);
// Building B: place and configure, but leave as a construction site. // Building B: place and configure, but leave as a construction site.
const BuildingId idB = const BuildingId idB =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East); SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East);
REQUIRE(idB != kInvalidBuildingId); REQUIRE(idB != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore"); SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
REQUIRE(sim.buildings().findSite(idB) != nullptr); REQUIRE(sim.getBuildings().findSite(idB) != nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB }); const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
@@ -779,7 +779,7 @@ TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction
const BuildingId id = const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildings().findSite(id) != nullptr); REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(selectionHasPlaceableBuilding(sim, { id })); REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
} }
@@ -859,7 +859,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
SimulationTestAccess::buildings(sim).setShipLayout(id, layout); SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
const ConstructionSite* site = sim.buildings().findSite(id); const ConstructionSite* site = sim.getBuildings().findSite(id);
REQUIRE(site != nullptr); REQUIRE(site != nullptr);
REQUIRE(site->shipLayout.has_value()); REQUIRE(site->shipLayout.has_value());
REQUIRE(site->shipLayout->placedModules.size() == 1); REQUIRE(site->shipLayout->placedModules.size() == 1);
@@ -885,7 +885,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
// Shipyard construction_time_seconds = 30 in the test config. // Shipyard construction_time_seconds = 30 in the test config.
double constructionTime = 0.0; double constructionTime = 0.0;
for (const BuildingDef& def : sim.config().buildings.buildings) for (const BuildingDef& def : sim.getConfig().buildings.buildings)
{ {
if (def.type == BuildingType::Shipyard) { constructionTime = def.constructionTimeSeconds; break; } if (def.type == BuildingType::Shipyard) { constructionTime = def.constructionTimeSeconds; break; }
} }
@@ -896,7 +896,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
sim.tick(); sim.tick();
} }
const Building* b = sim.buildings().findBuilding(id); const Building* b = sim.getBuildings().findBuilding(id);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
REQUIRE(b->shipLayout.has_value()); REQUIRE(b->shipLayout.has_value());
REQUIRE(b->shipLayout->placedModules.size() == 1); REQUIRE(b->shipLayout->placedModules.size() == 1);

View File

@@ -114,8 +114,8 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
const BuildingId id = const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildings().findBuilding(id) == nullptr); REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
REQUIRE(sim.buildings().findSite(id) != nullptr); REQUIRE(sim.getBuildings().findSite(id) != nullptr);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");

View File

@@ -140,7 +140,7 @@ TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[
// row sits above the world. // row sits above the world.
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0); const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId); REQUIRE(id == kInvalidBuildingId);
REQUIRE(f.bs.allSites().empty()); REQUIRE(f.bs.getAllSites().empty());
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0))); REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
} }
@@ -154,7 +154,7 @@ TEST_CASE("BuildingSystem: place rejects a building below the world (y >= height
const BuildingId id = f.bs.place(BuildingType::Miner, const BuildingId id = f.bs.place(BuildingType::Miner,
QPoint(0, heightTiles - 1), Rotation::East, 0); QPoint(0, heightTiles - 1), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId); REQUIRE(id == kInvalidBuildingId);
REQUIRE(f.bs.allSites().empty()); REQUIRE(f.bs.getAllSites().empty());
} }
TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge", "[building]") TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge", "[building]")
@@ -165,7 +165,7 @@ TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge",
const BuildingId id = f.bs.place(BuildingType::Miner, const BuildingId id = f.bs.place(BuildingType::Miner,
QPoint(leftEdgeX - 1, 0), Rotation::East, 0); QPoint(leftEdgeX - 1, 0), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId); REQUIRE(id == kInvalidBuildingId);
REQUIRE(f.bs.allSites().empty()); REQUIRE(f.bs.getAllSites().empty());
} }
TEST_CASE("BuildingSystem: place accepts a building flush against the world's left edge", TEST_CASE("BuildingSystem: place accepts a building flush against the world's left edge",
@@ -237,9 +237,9 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East));
REQUIRE(bs.allBuildings().size() == 1); REQUIRE(bs.getAllBuildings().size() == 1);
REQUIRE(bs.allBuildings()[0].type == BuildingType::Belt); REQUIRE(bs.getAllBuildings()[0].type == BuildingType::Belt);
REQUIRE(bs.allBuildings()[0].anchor == QPoint(5, 5)); REQUIRE(bs.getAllBuildings()[0].anchor == QPoint(5, 5));
} }
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]") TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")
@@ -258,8 +258,8 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
REQUIRE(bs.allSites().size() == 1); REQUIRE(bs.getAllSites().size() == 1);
REQUIRE(bs.allBuildings().empty()); REQUIRE(bs.getAllBuildings().empty());
REQUIRE(bs.findSite(id) != nullptr); REQUIRE(bs.findSite(id) != nullptr);
} }
@@ -289,7 +289,7 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]
// Miner cost = 15, refund = floor(15 * 75 / 100) = 11. // Miner cost = 15, refund = floor(15 * 75 / 100) = 11.
REQUIRE(refund == 15 * cfg.world.refundPercentage / 100); REQUIRE(refund == 15 * cfg.world.refundPercentage / 100);
REQUIRE_FALSE(bs.isTileOccupied(QPoint(0, 0))); REQUIRE_FALSE(bs.isTileOccupied(QPoint(0, 0)));
REQUIRE(bs.allSites().empty()); REQUIRE(bs.getAllSites().empty());
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -312,7 +312,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
rng); rng);
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
REQUIRE(bs.allSites().front().completesAt > 0); REQUIRE(bs.getAllSites().front().completesAt > 0);
} }
TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]") TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]")
@@ -332,9 +332,9 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0); bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0);
REQUIRE(bs.allSites().size() == 2); REQUIRE(bs.getAllSites().size() == 2);
REQUIRE(bs.allSites()[0].completesAt > 0); REQUIRE(bs.getAllSites()[0].completesAt > 0);
REQUIRE(bs.allSites()[1].completesAt == 0); REQUIRE(bs.getAllSites()[1].completesAt == 0);
} }
TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]") TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]")
@@ -358,7 +358,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
Tick tick = 0; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.allSites().empty()); REQUIRE(bs.getAllSites().empty());
REQUIRE(bs.findBuilding(id) != nullptr); REQUIRE(bs.findBuilding(id) != nullptr);
} }
@@ -383,9 +383,9 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
Tick tick = 0; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.allSites().size() == 1); REQUIRE(bs.getAllSites().size() == 1);
REQUIRE(bs.allSites().front().id == id2); REQUIRE(bs.getAllSites().front().id == id2);
REQUIRE(bs.allSites().front().completesAt > 0); REQUIRE(bs.getAllSites().front().completesAt > 0);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -457,7 +457,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
// Both produced items are held on the output side (buffer + emerging lane), // Both produced items are held on the output side (buffer + emerging lane),
// which is what the capacity rule counts (REQ-MAT-OUTPUT-EMERGE). // which is what the capacity rule counts (REQ-MAT-OUTPUT-EMERGE).
REQUIRE(b->outputItemCount() == 2); REQUIRE(b->getOutputItemCount() == 2);
REQUIRE_FALSE(b->production.has_value()); REQUIRE_FALSE(b->production.has_value());
} }
@@ -485,23 +485,23 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
Tick tick = 0; Tick tick = 0;
// Both still under construction. // Both still under construction.
REQUIRE(bs.productionBuildingCount() == 0); REQUIRE(bs.getProductionBuildingCount() == 0);
// The queue builds one at a time: miner (10s) completes at tick 300, then // The queue builds one at a time: miner (10s) completes at tick 300, then
// the smelter (15s) starts and completes at tick 300 + 450 = 750. // the smelter (15s) starts and completes at tick 300 + 450 = 750.
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.productionBuildingCount() == 1); REQUIRE(bs.getProductionBuildingCount() == 1);
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
REQUIRE(bs.productionBuildingCount() == 2); REQUIRE(bs.getProductionBuildingCount() == 2);
// Neither is producing yet: the miner has no recipe selected, and the // Neither is producing yet: the miner has no recipe selected, and the
// smelter (auto-recipe, REQ-BLD-SMELTER) has no input feeding it. // smelter (auto-recipe, REQ-BLD-SMELTER) has no input feeding it.
REQUIRE(bs.activeProductionBuildingCount() == 0); REQUIRE(bs.getActiveProductionBuildingCount() == 0);
bs.setRecipe(minerId, "mine_iron_ore"); bs.setRecipe(minerId, "mine_iron_ore");
runTicks(bs, belts, 1, tick); runTicks(bs, belts, 1, tick);
REQUIRE(bs.activeProductionBuildingCount() == 1); REQUIRE(bs.getActiveProductionBuildingCount() == 1);
} }
TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state", TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state",
@@ -524,11 +524,11 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
Tick tick = 0; Tick tick = 0;
// Not yet operational while under construction. // Not yet operational while under construction.
REQUIRE(bs.activeProductionBuildingCount() == 0); REQUIRE(bs.getActiveProductionBuildingCount() == 0);
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330). // Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.activeProductionBuildingCount() == 1); REQUIRE(bs.getActiveProductionBuildingCount() == 1);
// Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the // Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the
// output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER). // output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER).
@@ -536,9 +536,9 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
const Building* b = bs.findBuilding(id); const Building* b = bs.findBuilding(id);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
REQUIRE(b->outputItemCount() == 2); REQUIRE(b->getOutputItemCount() == 2);
REQUIRE_FALSE(b->production.has_value()); REQUIRE_FALSE(b->production.has_value());
REQUIRE(bs.activeProductionBuildingCount() == 0); REQUIRE(bs.getActiveProductionBuildingCount() == 0);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -882,7 +882,7 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu
REQUIRE(sink != nullptr); REQUIRE(sink != nullptr);
// Nothing was delivered, and the producer's output side has backed up to its cap. // Nothing was delivered, and the producer's output side has backed up to its cap.
REQUIRE(sink->pendingInputCount(ItemType{"iron_ore"}) == 0); REQUIRE(sink->pendingInputCount(ItemType{"iron_ore"}) == 0);
REQUIRE(miner->outputItemCount() == miner->outputBuffer.capacity); REQUIRE(miner->getOutputItemCount() == miner->outputBuffer.capacity);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -916,7 +916,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
{ {
const Building* b = bs.findBuilding(id); const Building* b = bs.findBuilding(id);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
REQUIRE(b->outputItemCount() > 0); REQUIRE(b->getOutputItemCount() > 0);
} }
bs.setRecipe(id, "mine_copper_ore"); bs.setRecipe(id, "mine_copper_ore");
@@ -924,7 +924,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
const Building* b = bs.findBuilding(id); const Building* b = bs.findBuilding(id);
// Clearing the output buffer on a recipe change also discards emerging items // Clearing the output buffer on a recipe change also discards emerging items
// (REQ-MAT-OUTPUT-EMERGE). // (REQ-MAT-OUTPUT-EMERGE).
REQUIRE(b->outputItemCount() == 0); REQUIRE(b->getOutputItemCount() == 0);
REQUIRE_FALSE(b->production.has_value()); REQUIRE_FALSE(b->production.has_value());
} }
@@ -1083,7 +1083,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
Tick tick = 0; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(bs.allSites().empty()); REQUIRE(bs.getAllSites().empty());
const std::optional<BuildingId> result = const std::optional<BuildingId> result =
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::South); bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::South);
@@ -1295,12 +1295,12 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
// Run until construction completes. // Run until construction completes.
Tick tick = 0; Tick tick = 0;
while (f.bs.allBuildings().empty() && tick < 100000) while (f.bs.getAllBuildings().empty() && tick < 100000)
{ {
runTicks(f.bs, f.belts, 1, tick); runTicks(f.bs, f.belts, 1, tick);
} }
REQUIRE(f.bs.allBuildings().size() == 1); REQUIRE(f.bs.getAllBuildings().size() == 1);
REQUIRE(f.bs.allBuildings()[0].type == BuildingType::Splitter); REQUIRE(f.bs.getAllBuildings()[0].type == BuildingType::Splitter);
// The built splitter is registered with BeltSystem carrying the filters. // The built splitter is registered with BeltSystem carrying the filters.
const std::optional<BeltSystem::SplitterInfo> builtInfo = f.belts.getSplitterInfo(tile); const std::optional<BeltSystem::SplitterInfo> builtInfo = f.belts.getSplitterInfo(tile);

View File

@@ -183,7 +183,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
// Find the player station entity via ECS. // Find the player station entity via ECS.
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
sim.admin().forEach<StationBodyComponent, FactionComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f) [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (!f.isEnemy && stationEntity == entt::null) if (!f.isEnemy && stationEntity == entt::null)
@@ -194,12 +194,12 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
sb.anchor.y() + sb.footprint.height() / 2.0f); sb.anchor.y() + sb.footprint.height() / 2.0f);
} }
}); });
REQUIRE(sim.admin().isValid(stationEntity)); REQUIRE(sim.getAdmin().isValid(stationEntity));
const ShipDef* combatDef = findCombatShip(sim.config()); const ShipDef* combatDef = findCombatShip(sim.getConfig());
REQUIRE(combatDef != nullptr); REQUIRE(combatDef != nullptr);
const entt::entity enemyShip = sim.ships().spawn( const entt::entity enemyShip = sim.getShips().spawn(
combatDef->id, combatDef->id,
QVector2D(stationCenter.x() + 1.0f, stationCenter.y()), QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
/*isEnemy=*/true); /*isEnemy=*/true);
@@ -221,7 +221,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
sim.admin().forEach<StationBodyComponent, FactionComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f) [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && stationEntity == entt::null) if (f.isEnemy && stationEntity == entt::null)
@@ -232,12 +232,12 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
sb.anchor.y() + sb.footprint.height() / 2.0f); sb.anchor.y() + sb.footprint.height() / 2.0f);
} }
}); });
REQUIRE(sim.admin().isValid(stationEntity)); REQUIRE(sim.getAdmin().isValid(stationEntity));
const ShipDef* combatDef = findCombatShip(sim.config()); const ShipDef* combatDef = findCombatShip(sim.getConfig());
REQUIRE(combatDef != nullptr); REQUIRE(combatDef != nullptr);
sim.ships().spawn( sim.getShips().spawn(
combatDef->id, combatDef->id,
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()), QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
/*isEnemy=*/false); /*isEnemy=*/false);
@@ -259,7 +259,7 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
sim.admin().forEach<StationBodyComponent, FactionComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f) [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && stationEntity == entt::null) if (f.isEnemy && stationEntity == entt::null)
@@ -270,12 +270,12 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
sb.anchor.y() + sb.footprint.height() / 2.0f); sb.anchor.y() + sb.footprint.height() / 2.0f);
} }
}); });
REQUIRE(sim.admin().isValid(stationEntity)); REQUIRE(sim.getAdmin().isValid(stationEntity));
const ShipDef* combatDef = findCombatShip(sim.config()); const ShipDef* combatDef = findCombatShip(sim.getConfig());
REQUIRE(combatDef != nullptr); REQUIRE(combatDef != nullptr);
const entt::entity playerShip = sim.ships().spawn( const entt::entity playerShip = sim.getShips().spawn(
combatDef->id, combatDef->id,
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()), QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
/*isEnemy=*/false); /*isEnemy=*/false);
@@ -390,17 +390,17 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* combatDef = findCombatShip(sim.config()); const ShipDef* combatDef = findCombatShip(sim.getConfig());
REQUIRE(combatDef != nullptr); REQUIRE(combatDef != nullptr);
const entt::entity ship = sim.ships().spawn(combatDef->id, const entt::entity ship = sim.getShips().spawn(combatDef->id,
QVector2D(10.0f, 10.0f)); QVector2D(10.0f, 10.0f));
sim.admin().get<HealthComponent>(ship).hp = -1.0f; sim.getAdmin().get<HealthComponent>(ship).hp = -1.0f;
sim.tick(); sim.tick();
REQUIRE_FALSE(sim.admin().isValid(ship)); REQUIRE_FALSE(sim.getAdmin().isValid(ship));
} }
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]") TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
@@ -411,15 +411,15 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
// (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat). The interceptor's // (REQ-RES-SCRAP-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.ships().spawn("interceptor", const entt::entity ship = sim.getShips().spawn("interceptor",
QVector2D(10.0f, 10.0f)); QVector2D(10.0f, 10.0f));
sim.admin().get<HealthComponent>(ship).hp = -1.0f; sim.getAdmin().get<HealthComponent>(ship).hp = -1.0f;
sim.tick(); sim.tick();
const std::vector<ScrapInfo> scraps = sim.scraps().allScrapInfo(); const std::vector<ScrapInfo> scraps = sim.getScraps().getAllScrapInfo();
REQUIRE(scraps.size() == 1); REQUIRE(scraps.size() == 1);
CHECK(sim.admin().get<ScrapDataComponent>(scraps[0].entity).amount == 59); CHECK(sim.getAdmin().get<ScrapDataComponent>(scraps[0].entity).amount == 59);
} }
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]") TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
@@ -427,7 +427,7 @@ TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
// Damage the HQ proxy entity (has HqProxy + Health). // Damage the HQ proxy entity (has HqProxy + Health).
sim.admin().forEach<HqProxyComponent, HealthComponent>( sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
[](entt::entity /*e*/, const HqProxyComponent& /*hq*/, HealthComponent& h) [](entt::entity /*e*/, const HqProxyComponent& /*hq*/, HealthComponent& h)
{ {
h.hp = -1.0f; h.hp = -1.0f;

View File

@@ -88,6 +88,11 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
REQUIRE(*cfg.world.buildingBlocksTooltip == REQUIRE(*cfg.world.buildingBlocksTooltip ==
"Spend building blocks to build; deliver them to the HQ to gain more."); "Spend building blocks to build; deliver them to the HQ to gain more.");
// Optional header artifact tooltip (REQ-UI-ARTIFACTS-TOOLTIP).
REQUIRE(cfg.world.artifactTooltip.has_value());
REQUIRE(*cfg.world.artifactTooltip ==
"Choose the artifact reward when destroying enemy stations; collect enough to win.");
// Spot-check that a config-derived formula computes as expected. // Spot-check that a config-derived formula computes as expected.
// threat_rate_formula = "x": evaluates to the input value. // threat_rate_formula = "x": evaluates to the input value.
REQUIRE(cfg.world.waves.threatRateFormula.evaluate(1.0) == Approx(1.0)); REQUIRE(cfg.world.waves.threatRateFormula.evaluate(1.0) == Approx(1.0));

View File

@@ -67,7 +67,7 @@ TEST_CASE("Hasher: identical inputs produce identical values", "[determinism]")
b.append(3.5f); b.append(3.5f);
b.append(std::string("ore")); b.append(std::string("ore"));
REQUIRE(a.value() == b.value()); REQUIRE(a.getValue() == b.getValue());
} }
TEST_CASE("Hasher: differing inputs produce differing values", "[determinism]") TEST_CASE("Hasher: differing inputs produce differing values", "[determinism]")
@@ -77,7 +77,7 @@ TEST_CASE("Hasher: differing inputs produce differing values", "[determinism]")
a.append(42); a.append(42);
b.append(43); b.append(43);
REQUIRE(a.value() != b.value()); REQUIRE(a.getValue() != b.getValue());
} }
TEST_CASE("Hasher: string concatenation does not collide", "[determinism]") TEST_CASE("Hasher: string concatenation does not collide", "[determinism]")
@@ -89,7 +89,7 @@ TEST_CASE("Hasher: string concatenation does not collide", "[determinism]")
b.append(std::string("a")); b.append(std::string("a"));
b.append(std::string("bc")); b.append(std::string("bc"));
REQUIRE(a.value() != b.value()); REQUIRE(a.getValue() != b.getValue());
} }
TEST_CASE("Hasher: negative and positive zero hash equally", "[determinism]") TEST_CASE("Hasher: negative and positive zero hash equally", "[determinism]")
@@ -99,7 +99,7 @@ TEST_CASE("Hasher: negative and positive zero hash equally", "[determinism]")
a.append(-0.0f); a.append(-0.0f);
b.append(0.0f); b.append(0.0f);
REQUIRE(a.value() == b.value()); REQUIRE(a.getValue() == b.getValue());
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -124,7 +124,7 @@ TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]
const Simulation a(loadConfig(), 777); const Simulation a(loadConfig(), 777);
const Simulation b(loadConfig(), 777); const Simulation b(loadConfig(), 777);
REQUIRE(a.rngFingerprint() == b.rngFingerprint()); REQUIRE(a.getRngFingerprint() == b.getRngFingerprint());
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -37,7 +37,7 @@ TEST_CASE("Formula retains its source string", "[formula]")
const std::string source = "10 + x / 5"; const std::string source = "10 + x / 5";
const Formula f = Formula::compile(source); const Formula f = Formula::compile(source);
REQUIRE(f.source() == source); REQUIRE(f.getSource() == source);
} }
TEST_CASE("Formula throws on malformed source", "[formula]") TEST_CASE("Formula throws on malformed source", "[formula]")

View File

@@ -22,7 +22,7 @@ static GameConfig loadConfig()
// tickDeathsAndLoot fires, triggering the push and schematic choices. // tickDeathsAndLoot fires, triggering the push and schematic choices.
static void killEnemyStations(Simulation& sim) static void killEnemyStations(Simulation& sim)
{ {
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health) [](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health)
{ {
if (faction.isEnemy) if (faction.isEnemy)

View File

@@ -166,7 +166,7 @@ TEST_CASE("a recorded run replays to byte-identical state with no desync", "[rep
manager.drain(); manager.drain();
for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); } for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); }
replayPath = recorderPtr->currentFilePath(); replayPath = recorderPtr->getCurrentFilePath();
recordedFinalChecksum = rec.computeStateChecksum(); recordedFinalChecksum = rec.computeStateChecksum();
manager.setRecorder(nullptr); // close the file manager.setRecorder(nullptr); // close the file
} }
@@ -185,11 +185,11 @@ TEST_CASE("a recorded run replays to byte-identical state with no desync", "[rep
while (!player.isFinished()) while (!player.isFinished())
{ {
play.tick(); play.tick();
player.advanceTo(play.currentTick()); player.advanceTo(play.getCurrentTick());
} }
REQUIRE_FALSE(player.getDesyncTick().has_value()); REQUIRE_FALSE(player.getDesyncTick().has_value());
REQUIRE(play.currentTick() == 150); REQUIRE(play.getCurrentTick() == 150);
REQUIRE(play.computeStateChecksum() == recordedFinalChecksum); REQUIRE(play.computeStateChecksum() == recordedFinalChecksum);
QFile::remove(QString::fromStdString(replayPath)); QFile::remove(QString::fromStdString(replayPath));
@@ -208,7 +208,7 @@ TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay
ReplayRecorder* recorderPtr = recorder.get(); ReplayRecorder* recorderPtr = recorder.get();
manager.setRecorder(std::move(recorder)); manager.setRecorder(std::move(recorder));
for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); } for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); }
replayPath = recorderPtr->currentFilePath(); replayPath = recorderPtr->getCurrentFilePath();
manager.setRecorder(nullptr); manager.setRecorder(nullptr);
} }
@@ -234,7 +234,7 @@ TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay
while (!player.isFinished()) while (!player.isFinished())
{ {
play.tick(); play.tick();
player.advanceTo(play.currentTick()); player.advanceTo(play.getCurrentTick());
} }
REQUIRE(player.getDesyncTick().has_value()); REQUIRE(player.getDesyncTick().has_value());
@@ -267,7 +267,7 @@ TEST_CASE("a long recorded run (through waves and combat) replays with no desync
manager.drain(); manager.drain();
for (int i = 0; i < 900; ++i) { rec.tick(); manager.recordTickCheckpoint(); } for (int i = 0; i < 900; ++i) { rec.tick(); manager.recordTickCheckpoint(); }
replayPath = recorderPtr->currentFilePath(); replayPath = recorderPtr->getCurrentFilePath();
recordedFinalChecksum = rec.computeStateChecksum(); recordedFinalChecksum = rec.computeStateChecksum();
manager.setRecorder(nullptr); manager.setRecorder(nullptr);
} }
@@ -281,11 +281,11 @@ TEST_CASE("a long recorded run (through waves and combat) replays with no desync
while (!player.isFinished()) while (!player.isFinished())
{ {
play.tick(); play.tick();
player.advanceTo(play.currentTick()); player.advanceTo(play.getCurrentTick());
} }
REQUIRE_FALSE(player.getDesyncTick().has_value()); REQUIRE_FALSE(player.getDesyncTick().has_value());
REQUIRE(play.currentTick() == 2400); REQUIRE(play.getCurrentTick() == 2400);
REQUIRE(play.computeStateChecksum() == recordedFinalChecksum); REQUIRE(play.computeStateChecksum() == recordedFinalChecksum);
QFile::remove(QString::fromStdString(replayPath)); QFile::remove(QString::fromStdString(replayPath));

View File

@@ -120,7 +120,7 @@ TEST_CASE("ReplayRecorder writes a well-formed file", "[replay]")
recorder.recordChecksum(30, 0x0ffffffffffffff0ull); recorder.recordChecksum(30, 0x0ffffffffffffff0ull);
const std::string path = recorder.currentFilePath(); const std::string path = recorder.getCurrentFilePath();
REQUIRE_FALSE(path.empty()); REQUIRE_FALSE(path.empty());
recorder.close(); recorder.close();
@@ -152,7 +152,7 @@ TEST_CASE("CommandManager records commands and an initial checksum on drain", "[
ReplayRecorder* recorderPtr = recorder.get(); ReplayRecorder* recorderPtr = recorder.get();
// setRecorder opens the file and writes the header + the tick-0 checksum. // setRecorder opens the file and writes the header + the tick-0 checksum.
manager.setRecorder(std::move(recorder)); manager.setRecorder(std::move(recorder));
const std::string path = recorderPtr->currentFilePath(); const std::string path = recorderPtr->getCurrentFilePath();
REQUIRE_FALSE(path.empty()); REQUIRE_FALSE(path.empty());
std::shared_ptr<PlaceBuildingCommand> place = std::make_shared<PlaceBuildingCommand>(); std::shared_ptr<PlaceBuildingCommand> place = std::make_shared<PlaceBuildingCommand>();
@@ -175,10 +175,10 @@ TEST_CASE("ReplayRecorder startNewRun rolls to a new file", "[replay]")
ReplayRecorder recorder(CONFIG_DIR, tempOutputDir()); ReplayRecorder recorder(CONFIG_DIR, tempOutputDir());
recorder.startNewRun(1u, 0ull); recorder.startNewRun(1u, 0ull);
const std::string first = recorder.currentFilePath(); const std::string first = recorder.getCurrentFilePath();
recorder.startNewRun(2u, 0ull); recorder.startNewRun(2u, 0ull);
const std::string second = recorder.currentFilePath(); const std::string second = recorder.getCurrentFilePath();
REQUIRE(first != second); REQUIRE(first != second);
REQUIRE(second.find("_2.replay") != std::string::npos); REQUIRE(second.find("_2.replay") != std::string::npos);
@@ -194,7 +194,7 @@ TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir()); std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
ReplayRecorder* recorderPtr = recorder.get(); ReplayRecorder* recorderPtr = recorder.get();
manager.setRecorder(std::move(recorder)); manager.setRecorder(std::move(recorder));
const std::string firstPath = recorderPtr->currentFilePath(); const std::string firstPath = recorderPtr->getCurrentFilePath();
std::shared_ptr<ResetCommand> reset = std::make_shared<ResetCommand>(); std::shared_ptr<ResetCommand> reset = std::make_shared<ResetCommand>();
reset->config = std::make_shared<GameConfig>(ConfigLoader::loadFromDirectory(CONFIG_DIR)); reset->config = std::make_shared<GameConfig>(ConfigLoader::loadFromDirectory(CONFIG_DIR));
@@ -202,7 +202,7 @@ TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
manager.enqueue(reset); manager.enqueue(reset);
manager.drain(); manager.drain();
const std::string secondPath = recorderPtr->currentFilePath(); const std::string secondPath = recorderPtr->getCurrentFilePath();
REQUIRE(firstPath != secondPath); REQUIRE(firstPath != secondPath);
REQUIRE(secondPath.find("_999.replay") != std::string::npos); REQUIRE(secondPath.find("_999.replay") != std::string::npos);

View File

@@ -148,7 +148,7 @@ TEST_CASE("ScrapSystem: allScrapInfo returns all spawned scrap", "[scrap]")
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.allScrapInfo(); const std::vector<ScrapInfo> info = ss.getAllScrapInfo();
REQUIRE(info.size() == 2); REQUIRE(info.size() == 2);
} }
@@ -160,7 +160,7 @@ TEST_CASE("ScrapSystem: allScrapInfo reports each pile's remaining amount", "[sc
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.allScrapInfo(); const std::vector<ScrapInfo> info = ss.getAllScrapInfo();
REQUIRE(info.size() == 2); REQUIRE(info.size() == 2);
for (const ScrapInfo& i : info) for (const ScrapInfo& i : info)
{ {

View File

@@ -87,7 +87,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
} }
for (const PlacedModule& pm : layout.placedModules) for (const PlacedModule& pm : layout.placedModules)
{ {
for (const ModuleDef& modDef : sim.config().modules.modules) for (const ModuleDef& modDef : sim.getConfig().modules.modules)
{ {
if (modDef.id == pm.moduleId) if (modDef.id == pm.moduleId)
{ {
@@ -109,22 +109,22 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]") TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const float expectedHp = static_cast<float>(def->health.hp); const float expectedHp = static_cast<float>(def->health.hp);
const entt::entity e = sim.ships().spawn("interceptor", const entt::entity e = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, std::nullopt); QVector2D(5.0f, 5.0f), false, std::nullopt);
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.getAdmin().isValid(e));
CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(expectedHp)); CHECK(sim.getAdmin().get<HealthComponent>(e).maxHp == Approx(expectedHp));
} }
TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]") TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const float baseHp = static_cast<float>(def->health.hp); const float baseHp = static_cast<float>(def->health.hp);
@@ -136,23 +136,23 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
const entt::entity e = sim.ships().spawn("interceptor", const entt::entity e = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.getAdmin().isValid(e));
// armor_plate has multiplied_hp_formula = "1.5" // armor_plate has multiplied_hp_formula = "1.5"
// final = base * (1 + (1.5 - 1)) + 0 = base * 1.5 // final = base * (1 + (1.5 - 1)) + 0 = base * 1.5
CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(baseHp * 1.5f)); CHECK(sim.getAdmin().get<HealthComponent>(e).maxHp == Approx(baseHp * 1.5f));
CHECK(sim.admin().get<HealthComponent>(e).hp == sim.admin().get<HealthComponent>(e).maxHp); CHECK(sim.getAdmin().get<HealthComponent>(e).hp == sim.getAdmin().get<HealthComponent>(e).maxHp);
} }
TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]") TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const float tileSize = static_cast<float>(sim.config().world.tileSize_m); const float tileSize = static_cast<float>(sim.getConfig().world.tileSize_m);
const float baseRange_tiles = static_cast<float>(def->sensor.sensorRange_m) / tileSize; const float baseRange_tiles = static_cast<float>(def->sensor.sensorRange_m) / tileSize;
ShipLayoutConfig layout; ShipLayoutConfig layout;
@@ -162,19 +162,19 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
const entt::entity e = sim.ships().spawn("interceptor", const entt::entity e = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.getAdmin().isValid(e));
// sensor_booster has added_sensor_range_m_formula = "100" m → 100/10 = 10 tiles // sensor_booster has added_sensor_range_m_formula = "100" m → 100/10 = 10 tiles
// final = baseRange_tiles * 1.0 + 10 = baseRange_tiles + 10 // final = baseRange_tiles * 1.0 + 10 = baseRange_tiles + 10
CHECK(sim.admin().get<SensorRangeComponent>(e).value_tiles == Approx(baseRange_tiles + 10.0f)); CHECK(sim.getAdmin().get<SensorRangeComponent>(e).value_tiles == Approx(baseRange_tiles + 10.0f));
} }
TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]") TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const float baseHp = static_cast<float>(def->health.hp); const float baseHp = static_cast<float>(def->health.hp);
@@ -189,14 +189,14 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
} }
const entt::entity e = sim.ships().spawn("interceptor", const entt::entity e = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.getAdmin().isValid(e));
// Two armor_plates: each 1.5 multiplier // Two armor_plates: each 1.5 multiplier
// total_mult = 1 + (1.5 - 1) + (1.5 - 1) = 2.0 // total_mult = 1 + (1.5 - 1) + (1.5 - 1) = 2.0
// final = base * 2.0 // final = base * 2.0
CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(baseHp * 2.0f)); CHECK(sim.getAdmin().get<HealthComponent>(e).maxHp == Approx(baseHp * 2.0f));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -207,7 +207,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -222,7 +222,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b = sim.buildings().findBuilding(yardId); const Building* b = sim.getBuildings().findBuilding(yardId);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board // armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10 // Total iron_ingot = 5, buffer cap = 2 * 5 = 10
@@ -234,9 +234,9 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -247,7 +247,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
fillMaterials(sim, yardId, *def, emptyLayout); fillMaterials(sim, yardId, *def, emptyLayout);
sim.tick(); sim.tick();
const Building* b1 = sim.buildings().findBuilding(yardId); const Building* b1 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b1 != nullptr); REQUIRE(b1 != nullptr);
REQUIRE(b1->production.has_value()); REQUIRE(b1->production.has_value());
@@ -261,7 +261,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b2 = sim.buildings().findBuilding(yardId); const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr); REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->production.has_value()); CHECK_FALSE(b2->production.has_value());
} }
@@ -270,7 +270,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
// The schematic carries a weapon in its (wave-only) default loadout. This // The schematic carries a weapon in its (wave-only) default loadout. This
// test pins that a player shipyard with no configured layout does NOT hand // test pins that a player shipyard with no configured layout does NOT hand
@@ -278,7 +278,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
// base-hull materials it was charged. // base-hull materials it was charged.
REQUIRE_FALSE(def->defaultModules.empty()); REQUIRE_FALSE(def->defaultModules.empty());
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -297,23 +297,23 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
// Locate the freshly built player ship. // Locate the freshly built player ship.
entt::entity built = entt::null; entt::entity built = entt::null;
sim.admin().forEach<ShipIdentityComponent, FactionComponent>( sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity e, const ShipIdentityComponent& si, const FactionComponent& fac) [&](entt::entity e, const ShipIdentityComponent& si, const FactionComponent& fac)
{ {
if (!fac.isEnemy && si.schematicId == "interceptor") { built = e; } if (!fac.isEnemy && si.schematicId == "interceptor") { built = e; }
}); });
REQUIRE(sim.admin().isValid(built)); REQUIRE(sim.getAdmin().isValid(built));
// Bare hull: the schematic's default weapon must NOT have been installed. // Bare hull: the schematic's default weapon must NOT have been installed.
const bool hasWeapon = const bool hasWeapon =
findFirstWeaponChild(sim.admin(), built) != entt::null; findFirstWeaponChild(sim.getAdmin(), built) != entt::null;
CHECK_FALSE(hasWeapon); CHECK_FALSE(hasWeapon);
} }
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]") TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -327,13 +327,13 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b1 = sim.buildings().findBuilding(yardId); const Building* b1 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b1 != nullptr); REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value()); REQUIRE(b1->shipLayout.has_value());
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer"); SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
const Building* b2 = sim.buildings().findBuilding(yardId); const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr); REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->shipLayout.has_value()); CHECK_FALSE(b2->shipLayout.has_value());
} }
@@ -342,7 +342,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -356,14 +356,14 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b1 = sim.buildings().findBuilding(yardId); const Building* b1 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b1 != nullptr); REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value()); REQUIRE(b1->shipLayout.has_value());
// Re-selecting the same recipe must be a no-op and preserve the layout. // Re-selecting the same recipe must be a no-op and preserve the layout.
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
const Building* b2 = sim.buildings().findBuilding(yardId); const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr); REQUIRE(b2 != nullptr);
REQUIRE(b2->shipLayout.has_value()); REQUIRE(b2->shipLayout.has_value());
REQUIRE(b2->shipLayout->placedModules.size() == 1); REQUIRE(b2->shipLayout->placedModules.size() == 1);
@@ -377,7 +377,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[modules]") TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
ShipLayoutConfig layout; ShipLayoutConfig layout;
@@ -390,22 +390,22 @@ TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[mo
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
} }
const entt::entity ship = sim.ships().spawn("interceptor", const entt::entity ship = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship); const entt::entity weapon = findFirstWeaponChild(sim.getAdmin(), ship);
REQUIRE(sim.admin().isValid(weapon)); REQUIRE(sim.getAdmin().isValid(weapon));
// base rate = 2.0 hz; weapon_primer multiplier = 1.2 → 2.4 hz // base rate = 2.0 hz; weapon_primer multiplier = 1.2 → 2.4 hz
CHECK(sim.admin().get<WeaponComponent>(weapon).fireRateHz == Approx(2.4f)); CHECK(sim.getAdmin().get<WeaponComponent>(weapon).fireRateHz == Approx(2.4f));
} }
TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation", "[modules]") TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const float tileSize = static_cast<float>(sim.config().world.tileSize_m); const float tileSize = static_cast<float>(sim.getConfig().world.tileSize_m);
ShipLayoutConfig layout; ShipLayoutConfig layout;
for (const std::string& id : {"laser_cannon", "weapon_stabilizer"}) for (const std::string& id : {"laser_cannon", "weapon_stabilizer"})
@@ -417,22 +417,22 @@ TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation",
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
} }
const entt::entity ship = sim.ships().spawn("interceptor", const entt::entity ship = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship); const entt::entity weapon = findFirstWeaponChild(sim.getAdmin(), ship);
REQUIRE(sim.admin().isValid(weapon)); REQUIRE(sim.getAdmin().isValid(weapon));
// base range = 50 m / tileSize = 5 tiles; weapon_stabilizer multiplier = 1.5 → 7.5 tiles // base range = 50 m / tileSize = 5 tiles; weapon_stabilizer multiplier = 1.5 → 7.5 tiles
CHECK(sim.admin().get<WeaponComponent>(weapon).range_tiles == Approx(50.0f / tileSize * 1.5f)); CHECK(sim.getAdmin().get<WeaponComponent>(weapon).range_tiles == Approx(50.0f / tileSize * 1.5f));
} }
TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s² to tiles/tick", "[modules]") TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s² to tiles/tick", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const float tileSize = static_cast<float>(sim.config().world.tileSize_m); const float tileSize = static_cast<float>(sim.getConfig().world.tileSize_m);
const float tickRate = static_cast<float>(kTickRateHz); const float tickRate = static_cast<float>(kTickRateHz);
const float base_mpss = static_cast<float>(def->movement.mainAcceleration_mpss); const float base_mpss = static_cast<float>(def->movement.mainAcceleration_mpss);
@@ -443,21 +443,21 @@ TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s²
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
const entt::entity e = sim.ships().spawn("interceptor", const entt::entity e = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
// added_main_acceleration_mpss = 60; same conversion as base: / tileSize / tickRate // added_main_acceleration_mpss = 60; same conversion as base: / tileSize / tickRate
const float expected = (base_mpss + 60.0f) / tileSize / tickRate; const float expected = (base_mpss + 60.0f) / tileSize / tickRate;
CHECK(sim.admin().get<DynamicBodyComponent>(e).mainAcceleration_tptt == Approx(expected)); CHECK(sim.getAdmin().get<DynamicBodyComponent>(e).mainAcceleration_tptt == Approx(expected));
} }
TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/tick", "[modules]") TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/tick", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const float tileSize = static_cast<float>(sim.config().world.tileSize_m); const float tileSize = static_cast<float>(sim.getConfig().world.tileSize_m);
const float tickRate = static_cast<float>(kTickRateHz); const float tickRate = static_cast<float>(kTickRateHz);
const float base_mpss = static_cast<float>(def->movement.maneuveringAcceleration_mpss); const float base_mpss = static_cast<float>(def->movement.maneuveringAcceleration_mpss);
@@ -468,12 +468,12 @@ TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration i
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
const entt::entity e = sim.ships().spawn("interceptor", const entt::entity e = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
// added_maneuvering_acceleration_mpss = 10; same conversion as base: / tileSize / tickRate // added_maneuvering_acceleration_mpss = 10; same conversion as base: / tileSize / tickRate
const float expected = (base_mpss + 10.0f) / tileSize / tickRate; const float expected = (base_mpss + 10.0f) / tileSize / tickRate;
CHECK(sim.admin().get<DynamicBodyComponent>(e).maneuveringAcceleration_tptt == Approx(expected)); CHECK(sim.getAdmin().get<DynamicBodyComponent>(e).maneuveringAcceleration_tptt == Approx(expected));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -56,7 +56,7 @@ static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
static int countShips(Simulation& sim) static int countShips(Simulation& sim)
{ {
int n = 0; int n = 0;
sim.admin().forEach<ShipIdentityComponent>( sim.getAdmin().forEach<ShipIdentityComponent>(
[&n](entt::entity /*e*/, const ShipIdentityComponent& /*si*/) { ++n; }); [&n](entt::entity /*e*/, const ShipIdentityComponent& /*si*/) { ++n; });
return n; return n;
} }
@@ -85,9 +85,9 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.config()); const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const int shipsBefore = countShips(sim); const int shipsBefore = countShips(sim);
@@ -115,7 +115,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
REQUIRE(countShips(sim) == shipsBefore + 1); REQUIRE(countShips(sim) == shipsBefore + 1);
bool foundPlayerShip = false; bool foundPlayerShip = false;
sim.admin().forEach<ShipIdentityComponent, FactionComponent>( sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f) [&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f)
{ {
if (!f.isEnemy && si.schematicId == def->id) if (!f.isEnemy && si.schematicId == def->id)
@@ -130,7 +130,7 @@ TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const int shipsBefore = countShips(sim); const int shipsBefore = countShips(sim);
@@ -146,9 +146,9 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.config()); const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const int shipsBefore = countShips(sim); const int shipsBefore = countShips(sim);
@@ -170,9 +170,9 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.config()); const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -203,7 +203,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
// Verify the shipyard production field cleared (i.e. the cycle completed // Verify the shipyard production field cleared (i.e. the cycle completed
// and is not still running). // and is not still running).
bool productionCleared = false; bool productionCleared = false;
for (const Building& b : sim.buildings().allBuildings()) for (const Building& b : sim.getBuildings().getAllBuildings())
{ {
if (b.id == yardId) if (b.id == yardId)
{ {

View File

@@ -19,7 +19,7 @@ TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
{ {
const Simulation sim(loadConfig()); const Simulation sim(loadConfig());
REQUIRE(sim.currentTick() == 0); REQUIRE(sim.getCurrentTick() == 0);
} }
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]") TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
@@ -28,7 +28,7 @@ TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
sim.tick(); sim.tick();
REQUIRE(sim.currentTick() == 1); REQUIRE(sim.getCurrentTick() == 1);
} }
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]") TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
@@ -40,7 +40,7 @@ TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
sim.tick(); sim.tick();
} }
REQUIRE(sim.currentTick() == 10); REQUIRE(sim.getCurrentTick() == 10);
} }
TEST_CASE("Simulation::drainBeamFiredEvents returns empty initially", "[simulation]") TEST_CASE("Simulation::drainBeamFiredEvents returns empty initially", "[simulation]")
@@ -131,3 +131,26 @@ TEST_CASE("TickDriver::reset clears the accumulator", "[simulation]")
// Nothing in the accumulator: zero elapsed time should not fire. // Nothing in the accumulator: zero elapsed time should not fire.
REQUIRE(driver.advance(0.0, 1.0) == 0); REQUIRE(driver.advance(0.0, 1.0) == 0);
} }
TEST_CASE("TickDriver::reset discards a large pending delta so a restart does not "
"fast-forward", "[simulation]")
{
// Regression guard for the "restart fast-forwards by the time spent in the
// Game Over / Win / escape dialog" bug: on restart the frame timer holds the
// whole wall-clock duration the modal was open. GameWorldView::resetForNewGame()
// now calls TickDriver::reset() to discard that pending delta; without it the
// fresh run would burst forward by the dialog duration on its first frame.
TickDriver driver;
// 30 seconds spent in the dialog would otherwise be ~900 ticks at 30 Hz.
const double dialogOpenMs = 30000.0;
driver.reset();
// A brand-new run's first normal frame (~16 ms) advances only its own ticks;
// the discarded dialog time contributes nothing.
REQUIRE(driver.advance(16.0, 1.0) == 0);
// Sanity: had the dialog delta not been discarded, it would have fired ~900 ticks.
TickDriver leaked;
REQUIRE(leaked.advance(dialogOpenMs, 1.0) > 800);
}

View File

@@ -22,8 +22,8 @@ class BuildingSystem;
// reached via buildings(sim)/belts(sim). // reached via buildings(sim)/belts(sim).
struct SimulationTestAccess struct SimulationTestAccess
{ {
static BuildingSystem& buildings(Simulation& sim) { return sim.buildingsMutable(); } static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); }
static BeltSystem& belts(Simulation& sim) { return sim.beltsMutable(); } static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); }
static BuildingId place(Simulation& sim, BuildingType type, QPoint anchor, static BuildingId place(Simulation& sim, BuildingType type, QPoint anchor,
Rotation rotation) Rotation rotation)

View File

@@ -67,7 +67,7 @@ RecipeDef& findRecipe(GameConfig& cfg, const std::string& id)
// tickDeathsAndLoot fires, triggering the push and schematic choices. // tickDeathsAndLoot fires, triggering the push and schematic choices.
void killEnemyStations(Simulation& sim) void killEnemyStations(Simulation& sim)
{ {
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health) [](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health)
{ {
if (faction.isEnemy) if (faction.isEnemy)

View File

@@ -49,7 +49,7 @@ TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
ws.tickThreatAccumulation(); ws.tickThreatAccumulation();
} }
REQUIRE(ws.threatLevel() == Approx(1.0)); REQUIRE(ws.getThreatLevel() == Approx(1.0));
} }
TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows", TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows",
@@ -60,7 +60,7 @@ TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside q
WaveSystem ws(cfg, rng); WaveSystem ws(cfg, rng);
// threat_rate_formula = "x", boss wave counter starts at 1 → rate = 1 threat/s. // threat_rate_formula = "x", boss wave counter starts at 1 → rate = 1 threat/s.
REQUIRE(ws.threatAccumulationRate() == Approx(1.0)); REQUIRE(ws.getThreatAccumulationRate() == Approx(1.0));
} }
TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]") TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]")
@@ -71,14 +71,14 @@ TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wav
std::mt19937 rng(42); std::mt19937 rng(42);
WaveSystem ws(cfg, rng); WaveSystem ws(cfg, rng);
REQUIRE(ws.threatAccumulationRate() == Approx(0.0)); REQUIRE(ws.getThreatAccumulationRate() == Approx(0.0));
const double before = ws.threatLevel(); const double before = ws.getThreatLevel();
for (int i = 0; i < static_cast<int>(secondsToTicks(1.0)); ++i) for (int i = 0; i < static_cast<int>(secondsToTicks(1.0)); ++i)
{ {
ws.tickThreatAccumulation(); ws.tickThreatAccumulation();
} }
REQUIRE(ws.threatLevel() == Approx(before)); REQUIRE(ws.getThreatLevel() == Approx(before));
} }
TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]") TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
@@ -87,11 +87,11 @@ TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruct
std::mt19937 rng(42); std::mt19937 rng(42);
WaveSystem ws(cfg, rng); WaveSystem ws(cfg, rng);
REQUIRE(ws.generation() == 0); REQUIRE(ws.getGeneration() == 0);
ws.onEnemyStationsDestroyed(); ws.onEnemyStationsDestroyed();
REQUIRE(ws.generation() == 1); REQUIRE(ws.getGeneration() == 1);
ws.onEnemyStationsDestroyed(); ws.onEnemyStationsDestroyed();
REQUIRE(ws.generation() == 2); REQUIRE(ws.getGeneration() == 2);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -104,7 +104,7 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
// HQ is still a Building (for belt integration). // HQ is still a Building (for belt integration).
int hqCount = 0; int hqCount = 0;
for (const Building& b : sim.buildings().allBuildings()) for (const Building& b : sim.getBuildings().getAllBuildings())
{ {
if (b.type == BuildingType::Hq) { ++hqCount; } if (b.type == BuildingType::Hq) { ++hqCount; }
} }
@@ -112,7 +112,7 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
// Stations are ECS entities. // Stations are ECS entities.
int playerCount = 0; int playerCount = 0;
int enemyCount = 0; int enemyCount = 0;
sim.admin().forEach<StationBodyComponent, FactionComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f) [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
{ {
if (f.isEnemy) { ++enemyCount; } if (f.isEnemy) { ++enemyCount; }
@@ -129,10 +129,10 @@ TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
const Simulation sim(loadConfig(), 42); const Simulation sim(loadConfig(), 42);
const float expectedHp = const float expectedHp =
static_cast<float>(sim.config().stations.hq.hpFormula.evaluate(0.0)); static_cast<float>(sim.getConfig().stations.hq.hpFormula.evaluate(0.0));
bool found = false; bool found = false;
float actualHp = 0.0f; float actualHp = 0.0f;
sim.admin().forEach<HqProxyComponent, HealthComponent>( sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
[&](entt::entity /*e*/, const HqProxyComponent& /*hq*/, const HealthComponent& h) [&](entt::entity /*e*/, const HqProxyComponent& /*hq*/, const HealthComponent& h)
{ {
found = true; found = true;
@@ -147,7 +147,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
{ {
const Simulation sim(loadConfig(), 42); const Simulation sim(loadConfig(), 42);
for (const Building& b : sim.buildings().allBuildings()) for (const Building& b : sim.getBuildings().getAllBuildings())
{ {
if (b.type != BuildingType::Hq) { continue; } if (b.type != BuildingType::Hq) { continue; }
// Rightmost body cell must be at x = -1 (asteroid right edge). // Rightmost body cell must be at x = -1 (asteroid right edge).
@@ -165,11 +165,11 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
int armedPlayerStations = 0; int armedPlayerStations = 0;
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>( sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo) [&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
{ {
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; } if (!sim.getAdmin().hasAll<StationBodyComponent>(mo.owner)) { return; }
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner); const FactionComponent& f = sim.getAdmin().get<FactionComponent>(mo.owner);
if (!f.isEnemy) if (!f.isEnemy)
{ {
++armedPlayerStations; ++armedPlayerStations;
@@ -186,11 +186,11 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
int armedEnemyStations = 0; int armedEnemyStations = 0;
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>( sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo) [&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
{ {
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; } if (!sim.getAdmin().hasAll<StationBodyComponent>(mo.owner)) { return; }
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner); const FactionComponent& f = sim.getAdmin().get<FactionComponent>(mo.owner);
if (f.isEnemy) if (f.isEnemy)
{ {
++armedEnemyStations; ++armedEnemyStations;
@@ -221,7 +221,7 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
sim.tick(); sim.tick();
if (!foundEnemyShip) if (!foundEnemyShip)
{ {
sim.admin().forEach<ShipIdentityComponent, FactionComponent>( sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const FactionComponent& f) const FactionComponent& f)
{ {
@@ -253,7 +253,7 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
// Damage both enemy stations to 0. // Damage both enemy stations to 0.
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
@@ -263,7 +263,7 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
// After push: should have 2 new enemy stations. // After push: should have 2 new enemy stations.
int enemyCount = 0; int enemyCount = 0;
sim.admin().forEach<StationBodyComponent, FactionComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f) [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
{ {
if (f.isEnemy) { ++enemyCount; } if (f.isEnemy) { ++enemyCount; }
@@ -275,7 +275,7 @@ TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
@@ -293,7 +293,7 @@ TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
@@ -306,20 +306,20 @@ TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
for (const SchematicChoiceOption& opt : choices) for (const SchematicChoiceOption& opt : choices)
{ {
bool validId = false; bool validId = false;
for (const ShipDef& def : sim.config().ships.ships) for (const ShipDef& def : sim.getConfig().ships.ships)
{ {
if (def.id == opt.schematicId) { validId = true; break; } if (def.id == opt.schematicId) { validId = true; break; }
} }
if (!validId) if (!validId)
{ {
for (const ModuleDef& def : sim.config().modules.modules) for (const ModuleDef& def : sim.getConfig().modules.modules)
{ {
if (def.id == opt.schematicId) { validId = true; break; } if (def.id == opt.schematicId) { validId = true; break; }
} }
} }
if (!validId) if (!validId)
{ {
for (const RecipeDef& def : sim.config().recipes.recipes) for (const RecipeDef& def : sim.getConfig().recipes.recipes)
{ {
if (def.id == opt.schematicId) { validId = true; break; } if (def.id == opt.schematicId) { validId = true; break; }
} }
@@ -332,7 +332,7 @@ TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
@@ -352,7 +352,7 @@ TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
@@ -370,7 +370,7 @@ TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
// Record the X position of the initial enemy stations. // Record the X position of the initial enemy stations.
int initialX = std::numeric_limits<int>::min(); int initialX = std::numeric_limits<int>::min();
sim.admin().forEach<StationBodyComponent, FactionComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f) [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && sb.anchor.x() > initialX) if (f.isEnemy && sb.anchor.x() > initialX)
@@ -379,7 +379,7 @@ TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
} }
}); });
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
@@ -387,7 +387,7 @@ TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
sim.tick(); sim.tick();
int newX = std::numeric_limits<int>::min(); int newX = std::numeric_limits<int>::min();
sim.admin().forEach<StationBodyComponent, FactionComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f) [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && sb.anchor.x() > newX) if (f.isEnemy && sb.anchor.x() > newX)

View File

@@ -12,12 +12,13 @@
#include "DisplayName.h" #include "DisplayName.h"
#include "EventManager.h" #include "EventManager.h"
#include "ExitBuilderModeRequestedEvent.h" #include "ExitBuilderModeRequestedEvent.h"
#include "Simulation.h"
BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent) BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWidget* parent)
: QWidget(parent) : QWidget(parent)
, m_sim(sim)
, m_config(config) , m_config(config)
, m_activeIndex(-1)
{ {
QGridLayout* layout = new QGridLayout(this); QGridLayout* layout = new QGridLayout(this);
layout->setSpacing(4); layout->setSpacing(4);
@@ -79,24 +80,42 @@ BuildButtonGrid::~BuildButtonGrid()
unregisterForEvents(); unregisterForEvents();
} }
void BuildButtonGrid::updateAffordability(int buildingBlocks) void BuildButtonGrid::updateAffordability()
{ {
const int buildingBlocks = m_sim->getBuildingBlocksStock();
// If the currently selected tool can no longer be afforded, exit builder mode
// before recomputing button states so it does not stay selected. Clearing the
// active index first lets the loop below disable the now-unaffordable button.
if (m_activeIndex)
{
const BuildingType activeType = m_types[*m_activeIndex];
const std::map<BuildingType, int>::const_iterator it = m_costs.find(activeType);
const int cost = (it != m_costs.end()) ? it->second : 0;
if (buildingBlocks < cost)
{
clearActiveButton();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBuilderModeRequestedEvent>());
}
}
for (std::size_t i = 0; i < m_buttons.size(); ++i) for (std::size_t i = 0; i < m_buttons.size(); ++i)
{ {
const BuildingType type = m_types[i]; const BuildingType type = m_types[i];
const std::map<BuildingType, int>::const_iterator it = m_costs.find(type); const std::map<BuildingType, int>::const_iterator it = m_costs.find(type);
const int cost = (it != m_costs.end()) ? it->second : 0; const int cost = (it != m_costs.end()) ? it->second : 0;
m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == static_cast<int>(i)); m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == i);
} }
} }
void BuildButtonGrid::clearActiveButton() void BuildButtonGrid::clearActiveButton()
{ {
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size())) if (m_activeIndex)
{ {
m_buttons[static_cast<std::size_t>(m_activeIndex)]->setChecked(false); m_buttons[*m_activeIndex]->setChecked(false);
} }
m_activeIndex = -1; m_activeIndex.reset();
} }
void BuildButtonGrid::onBuildButton(int index) void BuildButtonGrid::onBuildButton(int index)
@@ -105,8 +124,9 @@ void BuildButtonGrid::onBuildButton(int index)
{ {
return; return;
} }
const std::size_t idx = static_cast<std::size_t>(index);
if (m_activeIndex == index) if (m_activeIndex == idx)
{ {
clearActiveButton(); clearActiveButton();
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
@@ -114,15 +134,15 @@ void BuildButtonGrid::onBuildButton(int index)
return; return;
} }
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size())) if (m_activeIndex)
{ {
m_buttons[static_cast<std::size_t>(m_activeIndex)]->setChecked(false); m_buttons[*m_activeIndex]->setChecked(false);
} }
m_activeIndex = index; m_activeIndex = idx;
m_buttons[static_cast<std::size_t>(index)]->setChecked(true); m_buttons[idx]->setChecked(true);
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<BuildingTypeSelectedEvent>(m_types[static_cast<std::size_t>(index)])); std::make_shared<BuildingTypeSelectedEvent>(m_types[idx]));
} }
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/) void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/)
@@ -130,6 +150,11 @@ void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent>
clearActiveButton(); clearActiveButton();
} }
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
{
updateAffordability();
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event)
{ {
m_demolishButton->setChecked(event->active); m_demolishButton->setChecked(event->active);

View File

@@ -1,46 +1,56 @@
#pragma once #pragma once
#include <map> #include <map>
#include <optional>
#include <vector> #include <vector>
#include <QWidget> #include <QWidget>
#include "BuilderModeExitedEvent.h" #include "BuilderModeExitedEvent.h"
#include "BuildHotkeyPressedEvent.h" #include "BuildHotkeyPressedEvent.h"
#include "BuildingBlocksChangedEvent.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "DemolishModeChangedEvent.h" #include "DemolishModeChangedEvent.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
class QPushButton; class QPushButton;
class Simulation;
class BuildButtonGrid : public QWidget, class BuildButtonGrid : public QWidget,
public CombinedEventHandler<BuilderModeExitedEvent, public CombinedEventHandler<BuilderModeExitedEvent,
DemolishModeChangedEvent, DemolishModeChangedEvent,
BuildHotkeyPressedEvent> BuildHotkeyPressedEvent,
BuildingBlocksChangedEvent>
{ {
Q_OBJECT Q_OBJECT
public: public:
BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr); BuildButtonGrid(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
~BuildButtonGrid() override; ~BuildButtonGrid() override;
void updateAffordability(int buildingBlocks);
void clearActiveButton(); void clearActiveButton();
private: private:
// Re-evaluates which build buttons are enabled from the current building block
// stock (read from the simulation). If the currently selected tool can no longer
// be afforded, it exits builder mode so the button does not stay selected.
void updateAffordability();
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override; void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override; void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override; void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
private slots: private slots:
void onBuildButton(int index); void onBuildButton(int index);
private: private:
Simulation* m_sim;
const GameConfig* m_config; const GameConfig* m_config;
std::vector<BuildingType> m_types; std::vector<BuildingType> m_types;
std::vector<QPushButton*> m_buttons; std::vector<QPushButton*> m_buttons;
std::map<BuildingType, int> m_costs; std::map<BuildingType, int> m_costs;
int m_activeIndex; std::optional<std::size_t> m_activeIndex;
QPushButton* m_demolishButton; QPushButton* m_demolishButton;
}; };

View File

@@ -19,6 +19,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPainterPath>
#include <QPen> #include <QPen>
#include <QPolygonF> #include <QPolygonF>
#include <QRadialGradient> #include <QRadialGradient>
@@ -211,7 +212,7 @@ void GameWorldView::onFrame()
if (m_replayPlayer->isFinished() if (m_replayPlayer->isFinished()
|| m_sim->isWon() || m_sim->isGameOver()) { break; } || m_sim->isWon() || m_sim->isGameOver()) { break; }
m_sim->tick(); m_sim->tick();
m_replayPlayer->advanceTo(m_sim->currentTick()); m_replayPlayer->advanceTo(m_sim->getCurrentTick());
} }
} }
else else
@@ -227,6 +228,12 @@ void GameWorldView::onFrame()
{ {
m_viewResetPending = false; m_viewResetPending = false;
resetForNewGame(); resetForNewGame();
// The reset command may have drained after a long-open modal (e.g. the
// Game Over dialog), so `elapsed` above still holds the whole time that
// dialog was open. Feeding it into the tick driver would fast-forward the
// brand-new run by that duration. resetForNewGame() has rebased the time
// source; skip this frame's tick advance so the stale delta is discarded.
return;
} }
// Notify presentation widgets that queued commands were applied, so a // Notify presentation widgets that queued commands were applied, so a
@@ -263,7 +270,7 @@ void GameWorldView::onFrame()
// Expire old beams. Lifetime is measured in game ticks so beams stay // Expire old beams. Lifetime is measured in game ticks so beams stay
// visible while the simulation is paused or slowed (REQ-SHP-FIRING-BEAM). // visible while the simulation is paused or slowed (REQ-SHP-FIRING-BEAM).
{ {
const Tick now = m_sim->currentTick(); const Tick now = m_sim->getCurrentTick();
std::vector<ActiveBeam> live; std::vector<ActiveBeam> live;
for (const ActiveBeam& b : m_activeBeams) for (const ActiveBeam& b : m_activeBeams)
{ {
@@ -314,11 +321,11 @@ void GameWorldView::onFrame()
// Fire events for any state that changed since the last frame // Fire events for any state that changed since the last frame
{ {
const Tick newTick = m_sim->currentTick(); const Tick newTick = m_sim->getCurrentTick();
const int newBlocks = m_sim->buildingBlocksStock(); const int newBlocks = m_sim->getBuildingBlocksStock();
const int newExpCost = m_sim->currentExpansionCost(); const int newExpCost = m_sim->getCurrentExpansionCost();
const int newBoss = m_sim->bossWaveCounter(); const int newBoss = m_sim->getBossWaveCounter();
const Tick newCountdown = m_sim->bossCountdownTicks(); const Tick newCountdown = m_sim->getBossCountdownTicks();
if (newTick != m_lastTick) if (newTick != m_lastTick)
{ {
@@ -384,16 +391,16 @@ void GameWorldView::onFrame()
// Artifact count is a passive reflection of sim state (not a live-input source // Artifact count is a passive reflection of sim state (not a live-input source
// like the schematic/game-over polls above), so it updates during replay too — // like the schematic/game-over polls above), so it updates during replay too —
// the recorded ApplySchematicChoice drives m_sim->artifactCount() forward and // the recorded ApplySchematicChoice drives m_sim->getArtifactCount() forward and
// the header bar must track it. Fires on the first frame (count 0 vs the -1 // the header bar must track it. Fires on the first frame (count 0 vs the -1
// sentinel), which also populates the win-count max (replacing the "0/?" label). // sentinel), which also populates the win-count max (replacing the "0/?" label).
const int currentArtifactCount = m_sim->artifactCount(); const int currentArtifactCount = m_sim->getArtifactCount();
if (currentArtifactCount != m_lastArtifactCount) if (currentArtifactCount != m_lastArtifactCount)
{ {
m_lastArtifactCount = currentArtifactCount; m_lastArtifactCount = currentArtifactCount;
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<ArtifactCountChangedEvent>( std::make_shared<ArtifactCountChangedEvent>(
currentArtifactCount, m_sim->config().world.artifacts.artifactWinCount)); currentArtifactCount, m_sim->getConfig().world.artifacts.artifactWinCount));
} }
update(); update();
@@ -425,6 +432,8 @@ void GameWorldView::paintGL()
drawBeams(painter); drawBeams(painter);
drawOverlays(painter); drawOverlays(painter);
drawScreenSpace(painter); drawScreenSpace(painter);
drawPauseBorder(painter);
drawDemolishBorder(painter);
drawReplayOverlay(painter); drawReplayOverlay(painter);
} }
@@ -432,27 +441,27 @@ void GameWorldView::paintGL()
// Coordinate helpers // Coordinate helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
float GameWorldView::tilePx() const float GameWorldView::getTilePx() const
{ {
if (m_config->world.heightTiles <= 0) { return 1.0f; } if (m_config->world.heightTiles <= 0) { return 1.0f; }
return static_cast<float>(height()) / static_cast<float>(m_config->world.heightTiles); return static_cast<float>(height()) / static_cast<float>(m_config->world.heightTiles);
} }
float GameWorldView::viewportWidthTiles() const float GameWorldView::getViewportWidthTiles() const
{ {
return static_cast<float>(width()) / tilePx(); return static_cast<float>(width()) / getTilePx();
} }
float GameWorldView::viewLeftTiles() const float GameWorldView::getViewLeftTiles() const
{ {
return m_scrollXTiles - viewportWidthTiles() / 2.0f; return m_scrollXTiles - getViewportWidthTiles() / 2.0f;
} }
QPointF GameWorldView::worldToWidget(QVector2D worldPos) const QPointF GameWorldView::worldToWidget(QVector2D worldPos) const
{ {
return QPointF( return QPointF(
static_cast<qreal>((worldPos.x() - viewLeftTiles()) * tilePx()), static_cast<qreal>((worldPos.x() - getViewLeftTiles()) * getTilePx()),
static_cast<qreal>(worldPos.y() * tilePx())); static_cast<qreal>(worldPos.y() * getTilePx()));
} }
QPointF GameWorldView::tileToWidget(QPoint tile) const QPointF GameWorldView::tileToWidget(QPoint tile) const
@@ -463,15 +472,15 @@ QPointF GameWorldView::tileToWidget(QPoint tile) const
QPoint GameWorldView::widgetToTile(QPoint widgetPt) const QPoint GameWorldView::widgetToTile(QPoint widgetPt) const
{ {
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + viewLeftTiles(); const float wx = static_cast<float>(widgetPt.x()) / getTilePx() + getViewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / tilePx(); const float wy = static_cast<float>(widgetPt.y()) / getTilePx();
return QPoint(static_cast<int>(std::floor(wx)), static_cast<int>(std::floor(wy))); return QPoint(static_cast<int>(std::floor(wx)), static_cast<int>(std::floor(wy)));
} }
QVector2D GameWorldView::widgetToWorld(QPoint widgetPt) const QVector2D GameWorldView::widgetToWorld(QPoint widgetPt) const
{ {
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + viewLeftTiles(); const float wx = static_cast<float>(widgetPt.x()) / getTilePx() + getViewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / tilePx(); const float wy = static_cast<float>(widgetPt.y()) / getTilePx();
return QVector2D(wx, wy); return QVector2D(wx, wy);
} }
@@ -479,22 +488,22 @@ QRectF GameWorldView::tileRect(QPoint tile) const
{ {
const QPointF tl = tileToWidget(tile); const QPointF tl = tileToWidget(tile);
return QRectF(tl.x(), tl.y(), return QRectF(tl.x(), tl.y(),
static_cast<qreal>(tilePx()), static_cast<qreal>(tilePx())); static_cast<qreal>(getTilePx()), static_cast<qreal>(getTilePx()));
} }
QRect GameWorldView::viewportRect() const QRect GameWorldView::getViewportRect() const
{ {
const int left = static_cast<int>(std::floor(viewLeftTiles())) - 1; const int left = static_cast<int>(std::floor(getViewLeftTiles())) - 1;
const int top = 0; const int top = 0;
const int right = static_cast<int>(std::ceil(viewLeftTiles() + viewportWidthTiles())) + 1; const int right = static_cast<int>(std::ceil(getViewLeftTiles() + getViewportWidthTiles())) + 1;
const int bottom = m_config->world.heightTiles; const int bottom = m_config->world.heightTiles;
return QRect(left, top, right - left, bottom - top); return QRect(left, top, right - left, bottom - top);
} }
float GameWorldView::asteroidLeftEdge() const float GameWorldView::getAsteroidLeftEdge() const
{ {
float leftX = -static_cast<float>(m_sim->currentAsteroidWidth_tiles()); float leftX = -static_cast<float>(m_sim->getCurrentAsteroidWidth_tiles());
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
{ {
@@ -507,11 +516,11 @@ float GameWorldView::asteroidLeftEdge() const
return leftX; return leftX;
} }
float GameWorldView::enemyStationRightEdge() const float GameWorldView::getEnemyStationRightEdge() const
{ {
float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth_tiles float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth_tiles
+ m_config->world.regions.contestZoneWidth_tiles); + m_config->world.regions.contestZoneWidth_tiles);
m_sim->admin().forEach<StationBodyComponent, FactionComponent>( m_sim->getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&rightX](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f) [&rightX](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (!f.isEnemy) { return; } if (!f.isEnemy) { return; }
@@ -546,7 +555,7 @@ float GameWorldView::panSpeedTilesPerSecondAt(float viewCenterXTiles) const
const float fast = static_cast<float>(m_config->world.scroll.panSpeedFast_tps); const float fast = static_cast<float>(m_config->world.scroll.panSpeedFast_tps);
const float half = static_cast<float>(m_config->world.scroll.panRampBandWidth_tiles) / 2.0f; const float half = static_cast<float>(m_config->world.scroll.panRampBandWidth_tiles) / 2.0f;
const float leftEdge = static_cast<float>(m_config->world.regions.playerBufferWidth_tiles); const float leftEdge = static_cast<float>(m_config->world.regions.playerBufferWidth_tiles);
const float rightEdge = enemyStationRightEdge(); const float rightEdge = getEnemyStationRightEdge();
// Rising ramp at the left boundary (slow -> fast) and falling ramp at the right // Rising ramp at the left boundary (slow -> fast) and falling ramp at the right
// boundary (fast -> slow); their minimum yields flat-slow outside, flat-fast in // boundary (fast -> slow); their minimum yields flat-slow outside, flat-fast in
@@ -562,8 +571,8 @@ void GameWorldView::clampScroll()
// m_scrollXTiles is the view center, so the pan limits are the edges themselves: // m_scrollXTiles is the view center, so the pan limits are the edges themselves:
// the view can pan left until the buildable/asteroid edge is centered, and right // the view can pan left until the buildable/asteroid edge is centered, and right
// until the enemy stations are centered (revealing a little space beyond them). // until the enemy stations are centered (revealing a little space beyond them).
const float leftBound = asteroidLeftEdge(); const float leftBound = getAsteroidLeftEdge();
const float rightBound = enemyStationRightEdge(); const float rightBound = getEnemyStationRightEdge();
m_scrollXTiles = std::max(leftBound, std::min(m_scrollXTiles, rightBound)); m_scrollXTiles = std::max(leftBound, std::min(m_scrollXTiles, rightBound));
} }
@@ -586,7 +595,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
// Terrain and world-bounds validity are owned by the simulation // Terrain and world-bounds validity are owned by the simulation
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy / // (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
// rotate-in-place check. // rotate-in-place check.
if (!m_sim->buildings().isPlacementValid(type, anchor, rot)) if (!m_sim->getBuildings().isPlacementValid(type, anchor, rot))
{ {
return false; return false;
} }
@@ -598,7 +607,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
bool anyOccupied = false; bool anyOccupied = false;
for (const QPoint& relCell : parsed.bodyCells) for (const QPoint& relCell : parsed.bodyCells)
{ {
if (m_sim->buildings().isTileOccupied(anchor + relCell)) if (m_sim->getBuildings().isTileOccupied(anchor + relCell))
{ {
anyOccupied = true; anyOccupied = true;
break; break;
@@ -607,14 +616,14 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
if (anyOccupied) if (anyOccupied)
{ {
return m_sim->buildings().findRotateInPlaceTarget(type, anchor, rot).has_value(); return m_sim->getBuildings().findRotateInPlaceTarget(type, anchor, rot).has_value();
} }
return true; return true;
} }
BuildingId GameWorldView::buildingAtTile(QPoint tile) const BuildingId GameWorldView::buildingAtTile(QPoint tile) const
{ {
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
{ {
@@ -629,7 +638,7 @@ BuildingId GameWorldView::buildingAtTile(QPoint tile) const
BuildingId GameWorldView::siteAtTile(QPoint tile) const BuildingId GameWorldView::siteAtTile(QPoint tile) const
{ {
for (const ConstructionSite& s : m_sim->buildings().allSites()) for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{ {
for (const QPoint& cell : s.bodyCells) for (const QPoint& cell : s.bodyCells)
{ {
@@ -651,7 +660,7 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
const int y1 = std::max(cornerA.y(), cornerB.y()); const int y1 = std::max(cornerA.y(), cornerB.y());
std::vector<BuildingId> ids; std::vector<BuildingId> ids;
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
{ {
@@ -663,7 +672,7 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
} }
} }
} }
for (const ConstructionSite& s : m_sim->buildings().allSites()) for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{ {
for (const QPoint& cell : s.bodyCells) for (const QPoint& cell : s.bodyCells)
{ {
@@ -680,11 +689,11 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) const std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) const
{ {
if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity)) if (!m_sim->getAdmin().isValid(entity) || !m_sim->getAdmin().hasAll<PositionComponent>(entity))
{ {
return std::nullopt; return std::nullopt;
} }
return m_sim->admin().get<PositionComponent>(entity).value; return m_sim->getAdmin().get<PositionComponent>(entity).value;
} }
void GameWorldView::clearScrapSelection() void GameWorldView::clearScrapSelection()
@@ -700,7 +709,7 @@ void GameWorldView::pruneDespawnedScrap()
if (m_selectedScrap.empty()) { return; } if (m_selectedScrap.empty()) { return; }
std::vector<entt::entity> live; std::vector<entt::entity> live;
for (const ScrapInfo& info : m_sim->scraps().allScrapInfo()) for (const ScrapInfo& info : m_sim->getScraps().getAllScrapInfo())
{ {
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity) if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity)
!= m_selectedScrap.end()) != m_selectedScrap.end())
@@ -747,7 +756,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
int totalCost = 0; int totalCost = 0;
for (const BlueprintBuilding& bb : bp.buildings) for (const BlueprintBuilding& bb : bp.buildings)
{ {
if (m_sim->buildings().findRotateInPlaceTarget( if (m_sim->getBuildings().findRotateInPlaceTarget(
bb.type, center + bb.offset, bb.rotation).has_value()) bb.type, center + bb.offset, bb.rotation).has_value())
{ {
continue; continue;
@@ -755,13 +764,13 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
const BuildingDef* def = findBuildingDef(bb.type); const BuildingDef* def = findBuildingDef(bb.type);
if (def) { totalCost += def->cost; } if (def) { totalCost += def->cost; }
} }
if (m_sim->buildingBlocksStock() < totalCost) { return; } if (m_sim->getBuildingBlocksStock() < totalCost) { return; }
for (const BlueprintBuilding& bb : bp.buildings) for (const BlueprintBuilding& bb : bp.buildings)
{ {
const QPoint anchor = center + bb.offset; const QPoint anchor = center + bb.offset;
const std::optional<BuildingId> rotateTarget = const std::optional<BuildingId> rotateTarget =
m_sim->buildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation); m_sim->getBuildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation);
if (rotateTarget.has_value()) if (rotateTarget.has_value())
{ {
std::shared_ptr<RotateInPlaceCommand> rotateCommand = std::shared_ptr<RotateInPlaceCommand> rotateCommand =
@@ -832,7 +841,7 @@ void GameWorldView::placeAtTile(QPoint tile)
} }
const std::optional<BuildingId> rotateTarget = const std::optional<BuildingId> rotateTarget =
m_sim->buildings().findRotateInPlaceTarget(type, tile, m_ghostRotation); m_sim->getBuildings().findRotateInPlaceTarget(type, tile, m_ghostRotation);
if (rotateTarget.has_value()) if (rotateTarget.has_value())
{ {
std::shared_ptr<RotateInPlaceCommand> command = std::shared_ptr<RotateInPlaceCommand> command =
@@ -853,7 +862,7 @@ void GameWorldView::placeAtTile(QPoint tile)
{ {
return; return;
} }
if (!m_sim->buildings().isTileOccupied(tile) && canAfford(type)) if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
{ {
enqueuePlaceBuilding(type, tile, m_ghostRotation); enqueuePlaceBuilding(type, tile, m_ghostRotation);
m_beltDragTiles.insert(tile); m_beltDragTiles.insert(tile);
@@ -863,7 +872,7 @@ void GameWorldView::placeAtTile(QPoint tile)
|| type == BuildingType::TunnelEntry || type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit) || type == BuildingType::TunnelExit)
{ {
if (!m_sim->buildings().isTileOccupied(tile) && canAfford(type)) if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
{ {
enqueuePlaceBuilding(type, tile, m_ghostRotation); enqueuePlaceBuilding(type, tile, m_ghostRotation);
if (type == BuildingType::TunnelEntry) if (type == BuildingType::TunnelEntry)
@@ -886,31 +895,39 @@ void GameWorldView::placeAtTile(QPoint tile)
// Port glyph helper // Port glyph helper
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void GameWorldView::drawPortGlyph(QPainter& painter, QPoint bodyTile, void GameWorldView::drawPortGlyph(QPainter& painter, QPoint tile,
Rotation direction, const QColor& color) Rotation direction, const QColor& color,
bool centered)
{ {
const float px = tilePx(); const float px = getTilePx();
const QRectF tr = tileRect(bodyTile); const QRectF tr = tileRect(tile);
const QPointF center(tr.x() + static_cast<qreal>(px) * 0.5, const QPointF center(tr.x() + static_cast<qreal>(px) * 0.5,
tr.y() + static_cast<qreal>(px) * 0.5); tr.y() + static_cast<qreal>(px) * 0.5);
QPointF offset; QPointF edgeOffset;
const char* ch; const char* ch;
switch (direction) switch (direction)
{ {
case Rotation::East: offset = QPointF(px * 0.25f, 0); ch = ">"; break; case Rotation::East: edgeOffset = QPointF(px * 0.25f, 0); ch = ">"; break;
case Rotation::West: offset = QPointF(-px * 0.25f, 0); ch = "<"; break; case Rotation::West: edgeOffset = QPointF(-px * 0.25f, 0); ch = "<"; break;
case Rotation::North: offset = QPointF(0, -px * 0.25f); ch = "^"; break; case Rotation::North: edgeOffset = QPointF(0, -px * 0.25f); ch = "^"; break;
case Rotation::South: offset = QPointF(0, px * 0.25f); ch = "v"; break; case Rotation::South: edgeOffset = QPointF(0, px * 0.25f); ch = "v"; break;
default: return; default: return;
} }
const qreal half = static_cast<qreal>(px) * 0.3; // Centered glyphs sit in the middle of the tile (used for a port's target
// cell, REQ-UI-PORT-TARGET-GLYPH) and are drawn 150% larger to stand out;
// otherwise offset toward the exit edge of the port's body tile
// (REQ-UI-PORT-GLYPH).
const QPointF offset = centered ? QPointF(0.0, 0.0) : edgeOffset;
const qreal glyphScale = centered ? 1.5 : 1.0;
const qreal half = static_cast<qreal>(px) * 0.3 * glyphScale;
const QPointF pos = center + offset; const QPointF pos = center + offset;
const QRectF textRect(pos.x() - half, pos.y() - half, half * 2.0, half * 2.0); const QRectF textRect(pos.x() - half, pos.y() - half, half * 2.0, half * 2.0);
QFont f = painter.font(); QFont f = painter.font();
f.setPixelSize(std::max(6, static_cast<int>(px * 0.4f))); f.setPixelSize(std::max(6, static_cast<int>(px * 0.4f * glyphScale)));
painter.setFont(f); painter.setFont(f);
painter.setPen(color); painter.setPen(color);
painter.drawText(textRect, Qt::AlignCenter, QString::fromLatin1(ch)); painter.drawText(textRect, Qt::AlignCenter, QString::fromLatin1(ch));
@@ -922,13 +939,13 @@ void GameWorldView::drawPortGlyph(QPainter& painter, QPoint bodyTile,
void GameWorldView::drawTiles(QPainter& painter) void GameWorldView::drawTiles(QPainter& painter)
{ {
const int leftTile = static_cast<int>(std::floor(viewLeftTiles())) - 1; const int leftTile = static_cast<int>(std::floor(getViewLeftTiles())) - 1;
const int rightTile = leftTile + static_cast<int>(std::ceil(viewportWidthTiles())) + 2; const int rightTile = leftTile + static_cast<int>(std::ceil(getViewportWidthTiles())) + 2;
const int bottomTile = m_config->world.heightTiles; const int bottomTile = m_config->world.heightTiles;
// Asteroid columns left of the buildable edge are not yet unlocked by // Asteroid columns left of the buildable edge are not yet unlocked by
// expansion; tint them so the player sees the reachable-but-locked area. // expansion; tint them so the player sees the reachable-but-locked area.
const int buildableLeftX = -m_sim->currentAsteroidWidth_tiles(); const int buildableLeftX = -m_sim->getCurrentAsteroidWidth_tiles();
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
for (int x = leftTile; x <= rightTile; ++x) for (int x = leftTile; x <= rightTile; ++x)
@@ -951,7 +968,7 @@ void GameWorldView::drawTiles(QPainter& painter)
void GameWorldView::drawBuildings(QPainter& painter) void GameWorldView::drawBuildings(QPainter& painter)
{ {
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
const std::map<BuildingType, BuildingVisuals>::const_iterator it = const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(b.type); m_visuals->buildings.find(b.type);
@@ -966,8 +983,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
const QPointF tl = tileToWidget(b.anchor); const QPointF tl = tileToWidget(b.anchor);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(tilePx()), b.footprint.width() * static_cast<qreal>(getTilePx()),
b.footprint.height() * static_cast<qreal>(tilePx())); b.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1)); painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -982,13 +999,13 @@ void GameWorldView::drawBuildings(QPainter& painter)
for (const Port& port : b.outputPorts) for (const Port& port : b.outputPorts)
{ {
drawPortGlyph(painter, portBodyTile(port.tile, port.direction), drawPortGlyph(painter, portBodyTile(port.tile, port.direction),
port.direction, bv.outline); port.direction, bv.outline, /*centered*/ false);
} }
// HP bar below the HQ footprint; the HQ's HP lives on its proxy entity. // HP bar below the HQ footprint; the HQ's HP lives on its proxy entity.
if (b.type == BuildingType::Hq) if (b.type == BuildingType::Hq)
{ {
m_sim->admin().forEach<HqProxyComponent, FactionComponent, HealthComponent>( m_sim->getAdmin().forEach<HqProxyComponent, FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const HqProxyComponent& /*hq*/, [&](entt::entity /*e*/, const HqProxyComponent& /*hq*/,
const FactionComponent& f, const HealthComponent& h) const FactionComponent& f, const HealthComponent& h)
{ {
@@ -1002,7 +1019,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
} }
painter.setOpacity(0.5); painter.setOpacity(0.5);
for (const ConstructionSite& s : m_sim->buildings().allSites()) for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{ {
const std::map<BuildingType, BuildingVisuals>::const_iterator it = const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(s.type); m_visuals->buildings.find(s.type);
@@ -1016,8 +1033,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
const QPointF tl = tileToWidget(s.anchor); const QPointF tl = tileToWidget(s.anchor);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
s.footprint.width() * static_cast<qreal>(tilePx()), s.footprint.width() * static_cast<qreal>(getTilePx()),
s.footprint.height() * static_cast<qreal>(tilePx())); s.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1, Qt::DashLine)); painter.setPen(QPen(bv.outline, 1, Qt::DashLine));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect); painter.drawRect(bboxRect);
@@ -1030,7 +1047,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
int pct = 0; int pct = 0;
if (s.completesAt > 0 && durationTicks > 0) if (s.completesAt > 0 && durationTicks > 0)
{ {
const Tick elapsed = m_sim->currentTick() const Tick elapsed = m_sim->getCurrentTick()
- (s.completesAt - durationTicks); - (s.completesAt - durationTicks);
pct = static_cast<int>( pct = static_cast<int>(
std::max(Tick(0), std::min(durationTicks, elapsed)) std::max(Tick(0), std::min(durationTicks, elapsed))
@@ -1061,7 +1078,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
{ {
const QPoint absBody = s.anchor const QPoint absBody = s.anchor
+ portBodyTile(port.tile, port.direction); + portBodyTile(port.tile, port.direction);
drawPortGlyph(painter, absBody, port.direction, bv.outline); drawPortGlyph(painter, absBody, port.direction, bv.outline,
/*centered*/ false);
} }
} }
} }
@@ -1079,12 +1097,12 @@ std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const
std::optional<QPoint> anchor; std::optional<QPoint> anchor;
std::optional<QSize> footprint; std::optional<QSize> footprint;
if (const Building* b = m_sim->buildings().findBuilding(id)) if (const Building* b = m_sim->getBuildings().findBuilding(id))
{ {
anchor = b->anchor; anchor = b->anchor;
footprint = b->footprint; footprint = b->footprint;
} }
else if (const ConstructionSite* s = m_sim->buildings().findSite(id)) else if (const ConstructionSite* s = m_sim->getBuildings().findSite(id))
{ {
anchor = s->anchor; anchor = s->anchor;
footprint = s->footprint; footprint = s->footprint;
@@ -1093,8 +1111,8 @@ std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const
const QPointF tl = tileToWidget(*anchor); const QPointF tl = tileToWidget(*anchor);
return QRectF(tl.x(), tl.y(), return QRectF(tl.x(), tl.y(),
footprint->width() * static_cast<qreal>(tilePx()), footprint->width() * static_cast<qreal>(getTilePx()),
footprint->height() * static_cast<qreal>(tilePx())); footprint->height() * static_cast<qreal>(getTilePx()));
} }
void GameWorldView::drawSelectionHighlights(QPainter& painter) void GameWorldView::drawSelectionHighlights(QPainter& painter)
@@ -1111,11 +1129,11 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
} }
// A ring around each selected scrap pile, sitting just outside the pile's // A ring around each selected scrap pile, sitting just outside the pile's
// rendered circle (radius tilePx()*0.2, matching drawScrap) (REQ-UI-SCRAP-CLICK-SELECT). // rendered circle (radius getTilePx()*0.2, matching drawScrap) (REQ-UI-SCRAP-CLICK-SELECT).
if (!m_selectedScrap.empty()) if (!m_selectedScrap.empty())
{ {
const qreal outlineRadius = static_cast<qreal>(tilePx() * 0.2f) + 3.0; const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo()) for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo())
{ {
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), scrap.entity) if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), scrap.entity)
== m_selectedScrap.end()) { continue; } == m_selectedScrap.end()) { continue; }
@@ -1136,13 +1154,13 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
painter.setBrush(color); painter.setBrush(color);
const BuildingType type = m_copiedConfig->type; const BuildingType type = m_copiedConfig->type;
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
if (b.type != type) { continue; } if (b.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(b.id); const std::optional<QRectF> rect = footprintWidgetRect(b.id);
if (rect.has_value()) { painter.drawRect(*rect); } if (rect.has_value()) { painter.drawRect(*rect); }
} }
for (const ConstructionSite& s : m_sim->buildings().allSites()) for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{ {
if (s.type != type) { continue; } if (s.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(s.id); const std::optional<QRectF> rect = footprintWidgetRect(s.id);
@@ -1163,7 +1181,7 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
void GameWorldView::drawPortItems(QPainter& painter) void GameWorldView::drawPortItems(QPainter& painter)
{ {
const float halfPx = tilePx() * 0.5f * 0.5f; const float halfPx = getTilePx() * 0.5f * 0.5f;
// Port items are drawn over the buildings (drawBuildings runs first) but clipped // Port items are drawn over the buildings (drawBuildings runs first) but clipped
// to a thin margin at each machine's edges: the clip region is the whole view // to a thin margin at each machine's edges: the clip region is the whole view
@@ -1174,10 +1192,10 @@ void GameWorldView::drawPortItems(QPainter& painter)
// (REQ-MAT-DIRECT-COUPLE). Transport tiles are not machines and never occlude, so // (REQ-MAT-DIRECT-COUPLE). Transport tiles are not machines and never occlude, so
// items on belts stay fully visible. // items on belts stay fully visible.
constexpr double kPortMarginTiles = 0.2; constexpr double kPortMarginTiles = 0.2;
const double margin = kPortMarginTiles * static_cast<double>(tilePx()); const double margin = kPortMarginTiles * static_cast<double>(getTilePx());
QRegion clip(rect()); QRegion clip(rect());
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
if (b.type == BuildingType::Belt || b.type == BuildingType::Splitter if (b.type == BuildingType::Belt || b.type == BuildingType::Splitter
|| b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit) || b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
@@ -1218,17 +1236,17 @@ void GameWorldView::drawPortItems(QPainter& painter)
painter.save(); painter.save();
painter.setClipRegion(clip); painter.setClipRegion(clip);
m_sim->buildings().forEachEmergingItem(drawItem); m_sim->getBuildings().forEachEmergingItem(drawItem);
m_sim->buildings().forEachIncomingItem(drawItem); m_sim->getBuildings().forEachIncomingItem(drawItem);
painter.restore(); painter.restore();
} }
void GameWorldView::drawBeltItems(QPainter& painter) void GameWorldView::drawBeltItems(QPainter& painter)
{ {
const float halfPx = tilePx() * 0.5f * 0.5f; const float halfPx = getTilePx() * 0.5f * 0.5f;
const QRect vr = viewportRect(); const QRect vr = getViewportRect();
m_sim->belts().forEachVisualItem(vr, [&](const VisualItem& vi) m_sim->getBelts().forEachVisualItem(vr, [&](const VisualItem& vi)
{ {
const std::map<std::string, ItemVisuals>::const_iterator it = const std::map<std::string, ItemVisuals>::const_iterator it =
m_visuals->items.find(vi.type.id); m_visuals->items.find(vi.type.id);
@@ -1248,8 +1266,8 @@ void GameWorldView::drawBeltItems(QPainter& painter)
void GameWorldView::drawScrap(QPainter& painter) void GameWorldView::drawScrap(QPainter& painter)
{ {
const float r = tilePx() * 0.2f; const float r = getTilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo()) for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo())
{ {
const QPointF center = worldToWidget(scrap.position); const QPointF center = worldToWidget(scrap.position);
painter.setBrush(QColor(128, 110, 90)); painter.setBrush(QColor(128, 110, 90));
@@ -1261,7 +1279,7 @@ void GameWorldView::drawScrap(QPainter& painter)
void GameWorldView::drawStations(QPainter& painter) void GameWorldView::drawStations(QPainter& painter)
{ {
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( m_sim->getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f,
const HealthComponent& h) const HealthComponent& h)
{ {
@@ -1281,8 +1299,8 @@ void GameWorldView::drawStations(QPainter& painter)
const QPointF tl = tileToWidget(QPoint(sb.anchor.x(), sb.anchor.y())); const QPointF tl = tileToWidget(QPoint(sb.anchor.x(), sb.anchor.y()));
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
sb.footprint.width() * static_cast<qreal>(tilePx()), sb.footprint.width() * static_cast<qreal>(getTilePx()),
sb.footprint.height() * static_cast<qreal>(tilePx())); sb.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1)); painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -1306,7 +1324,7 @@ void GameWorldView::drawStations(QPainter& painter)
void GameWorldView::drawShips(QPainter& painter) void GameWorldView::drawShips(QPainter& painter)
{ {
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>( FactionComponent, HealthComponent>(
[&](entt::entity e, const ShipIdentityComponent& si, [&](entt::entity e, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& facing, const PositionComponent& pos, const FacingComponent& facing,
@@ -1320,8 +1338,8 @@ void GameWorldView::drawShips(QPainter& painter)
const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians)); const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians));
const QVector2D perp(-dir.y(), dir.x()); const QVector2D perp(-dir.y(), dir.x());
const float fwd = tilePx() * 0.45f; const float fwd = getTilePx() * 0.45f;
const float side = tilePx() * 0.25f; const float side = getTilePx() * 0.25f;
QPolygonF tri; QPolygonF tri;
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd), tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
@@ -1356,7 +1374,7 @@ void GameWorldView::drawShips(QPainter& painter)
void GameWorldView::drawHpBar(QPainter& painter, qreal left, qreal top, qreal width, void GameWorldView::drawHpBar(QPainter& painter, qreal left, qreal top, qreal width,
float fraction, bool isEnemy) float fraction, bool isEnemy)
{ {
const qreal barH = static_cast<qreal>(tilePx()) * 0.12; const qreal barH = static_cast<qreal>(getTilePx()) * 0.12;
const float clamped = std::max(0.0f, fraction); const float clamped = std::max(0.0f, fraction);
painter.fillRect(QRectF(left, top, width, barH), QColor(60, 60, 60)); painter.fillRect(QRectF(left, top, width, barH), QColor(60, 60, 60));
painter.fillRect(QRectF(left, top, width * static_cast<qreal>(clamped), barH), painter.fillRect(QRectF(left, top, width * static_cast<qreal>(clamped), barH),
@@ -1366,7 +1384,7 @@ void GameWorldView::drawHpBar(QPainter& painter, qreal left, qreal top, qreal wi
void GameWorldView::drawDebugSensorRanges(QPainter& painter) void GameWorldView::drawDebugSensorRanges(QPainter& painter)
{ {
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, SensorRangeComponent>( FactionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& /*facing*/, const PositionComponent& pos, const FacingComponent& /*facing*/,
@@ -1378,7 +1396,7 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
const QPointF center = worldToWidget(pos.value); const QPointF center = worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles) const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(tilePx()); * static_cast<qreal>(getTilePx());
QColor circleColor = it->second.outline; QColor circleColor = it->second.outline;
circleColor.setAlpha(77); circleColor.setAlpha(77);
painter.setPen(QPen(circleColor, 1)); painter.setPen(QPen(circleColor, 1));
@@ -1404,7 +1422,7 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
painter.drawLine(worldToWidget(from), worldToWidget(to)); painter.drawLine(worldToWidget(from), worldToWidget(to));
}; };
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, AttackBehavior>( m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, AttackBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const AttackBehavior& attack) const PositionComponent& pos, const AttackBehavior& attack)
{ {
@@ -1417,7 +1435,7 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
drawTargetLine(si.schematicId, pos.value, *targetPos); drawTargetLine(si.schematicId, pos.value, *targetPos);
}); });
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, RepairBehavior>( m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, RepairBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const RepairBehavior& repair) const PositionComponent& pos, const RepairBehavior& repair)
{ {
@@ -1430,7 +1448,7 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
drawTargetLine(si.schematicId, pos.value, *targetPos); drawTargetLine(si.schematicId, pos.value, *targetPos);
}); });
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, SalvageScrapBehavior>( m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, SalvageScrapBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const SalvageScrapBehavior& salvage) const PositionComponent& pos, const SalvageScrapBehavior& salvage)
{ {
@@ -1446,15 +1464,15 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
const QStringList lines = { const QStringList lines = {
tr("Accumulated Threat Level: %1") tr("Accumulated Threat Level: %1")
.arg(m_sim->threatLevel(), 0, 'f', 1), .arg(m_sim->getThreatLevel(), 0, 'f', 1),
tr("Time until Wave: %1s") tr("Time until Wave: %1s")
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1), .arg(ticksToSeconds(m_sim->getNormalGapRemainingTicks()), 0, 'f', 1),
tr("Threat Accumulation Rate: %1 threat/s") tr("Threat Accumulation Rate: %1 threat/s")
.arg(m_sim->threatAccumulationRate(), 0, 'f', 1), .arg(m_sim->getThreatAccumulationRate(), 0, 'f', 1),
tr("Max Factory Production: %1 threat/s") tr("Max Factory Production: %1 threat/s")
.arg(m_sim->maxFactoryProductionThreatRate(), 0, 'f', 1), .arg(m_sim->getMaxFactoryProductionThreatRate(), 0, 'f', 1),
tr("Current Factory Production: %1 threat/s") tr("Current Factory Production: %1 threat/s")
.arg(m_sim->currentFactoryProductionThreatRate(), 0, 'f', 1), .arg(m_sim->getCurrentFactoryProductionThreatRate(), 0, 'f', 1),
}; };
QFont font = painter.font(); QFont font = painter.font();
@@ -1562,7 +1580,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
if (m_builderType.has_value()) if (m_builderType.has_value())
{ {
drawBuildingGhost(painter, *m_builderType, m_ghostTile, drawBuildingGhost(painter, *m_builderType, m_ghostTile,
m_ghostRotation, m_ghostValid); m_ghostRotation, m_ghostValid,
/*showPortTargetGlyphs*/ true);
} }
// Blueprint placement ghost // Blueprint placement ghost
@@ -1572,7 +1591,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
{ {
const QPoint anchor = m_blueprintGhostTile + bb.offset; const QPoint anchor = m_blueprintGhostTile + bb.offset;
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation); const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid); drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid,
/*showPortTargetGlyphs*/ false);
} }
} }
@@ -1582,12 +1602,12 @@ void GameWorldView::drawOverlays(QPainter& painter)
{ {
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile)) for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
{ {
const Building* b = m_sim->buildings().findBuilding(id); const Building* b = m_sim->getBuildings().findBuilding(id);
if (b && b->type == BuildingType::Hq) { continue; } if (b && b->type == BuildingType::Hq) { continue; }
const std::vector<QPoint>* cells = nullptr; const std::vector<QPoint>* cells = nullptr;
const ConstructionSite* s = nullptr; const ConstructionSite* s = nullptr;
if (b) { cells = &b->bodyCells; } if (b) { cells = &b->bodyCells; }
else if ((s = m_sim->buildings().findSite(id))) { cells = &s->bodyCells; } else if ((s = m_sim->getBuildings().findSite(id))) { cells = &s->bodyCells; }
if (cells) if (cells)
{ {
for (const QPoint& cell : *cells) for (const QPoint& cell : *cells)
@@ -1599,7 +1619,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
} }
else if (m_demolishMode && m_demolishHoverBuildingId != kInvalidBuildingId) else if (m_demolishMode && m_demolishHoverBuildingId != kInvalidBuildingId)
{ {
const Building* b = m_sim->buildings().findBuilding(m_demolishHoverBuildingId); const Building* b = m_sim->getBuildings().findBuilding(m_demolishHoverBuildingId);
if (b) if (b)
{ {
for (const QPoint& cell : b->bodyCells) for (const QPoint& cell : b->bodyCells)
@@ -1625,7 +1645,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type, void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation, QPoint anchorTile, Rotation rotation,
bool valid) bool valid, bool showPortTargetGlyphs)
{ {
const BuildingDef* def = findBuildingDef(type); const BuildingDef* def = findBuildingDef(type);
if (!def) { return; } if (!def) { return; }
@@ -1663,8 +1683,8 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
const QPointF tl = tileToWidget(anchorTile + minCell); const QPointF tl = tileToWidget(anchorTile + minCell);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
(maxCell.x() - minCell.x() + 1) * static_cast<qreal>(tilePx()), (maxCell.x() - minCell.x() + 1) * static_cast<qreal>(getTilePx()),
(maxCell.y() - minCell.y() + 1) * static_cast<qreal>(tilePx())); (maxCell.y() - minCell.y() + 1) * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(lineColor, 1)); painter.setPen(QPen(lineColor, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -1679,7 +1699,24 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
for (const Port& port : parsed.outputPorts) for (const Port& port : parsed.outputPorts)
{ {
drawPortGlyph(painter, anchorTile + portBodyTile(port.tile, port.direction), drawPortGlyph(painter, anchorTile + portBodyTile(port.tile, port.direction),
port.direction, lineColor); port.direction, lineColor, /*centered*/ false);
}
// REQ-UI-PORT-TARGET-GLYPH: while in builder mode, additionally mark each
// output port's target cell (the cell just outside the footprint the port
// pushes into) with a directional glyph, previewing where output will flow.
// The Tunnel Entry is excluded — it receives items from any of its non-mouth
// edges rather than emitting into a single adjacent cell. The Shipyard is
// excluded too — it spawns a ship rather than emitting a belt item.
if (showPortTargetGlyphs
&& type != BuildingType::TunnelEntry
&& type != BuildingType::Shipyard)
{
for (const Port& port : parsed.outputPorts)
{
drawPortGlyph(painter, anchorTile + port.tile, port.direction,
lineColor, /*centered*/ true);
}
} }
painter.setOpacity(1.0); painter.setOpacity(1.0);
@@ -1689,6 +1726,79 @@ void GameWorldView::drawScreenSpace(QPainter& /*painter*/)
{ {
} }
void GameWorldView::drawPauseBorder(QPainter& painter)
{
if (m_gameSpeedMultiplier > 0.0) { return; }
drawVignetteBorder(painter, QColor(0, 0, 0, 128)); // 50% black at the edge
}
void GameWorldView::drawDemolishBorder(QPainter& painter)
{
if (!m_demolishMode) { return; }
// Reuse the demolish overlay color (with its configured alpha) as the edge color.
drawVignetteBorder(painter, m_visuals->overlays.demolishTint);
}
void GameWorldView::drawVignetteBorder(QPainter& painter, const QColor& edgeColor)
{
// Border thickness in pixels, capped so it never exceeds half the viewport on
// very small windows (which would leave no un-tinted center).
const qreal borderPx = std::min<qreal>(
100.0, std::min(width(), height()) / 2.0);
if (borderPx <= 0.0) { return; }
const QColor& edge = edgeColor; // full color (and alpha) at the viewport edge
QColor inner = edgeColor;
inner.setAlpha(0); // same color, fully transparent toward the center
const qreal w = width();
const qreal h = height();
painter.save();
// Each side is a linear gradient fading from `edge` at the viewport border to
// `inner` toward the center. The four sides are clipped to trapezoids that meet
// along the corner diagonals (a mitred picture frame), so the strips never
// overlap and don't double-darken the corners. Because the border thickness is
// the same on every side, both adjoining gradients evaluate equally along each
// 45-degree diagonal and join seamlessly.
const std::function<void(const QLinearGradient&, const QPolygonF&)> drawSide =
[&](const QLinearGradient& gradient, const QPolygonF& trapezoid) {
QPainterPath clip;
clip.addPolygon(trapezoid);
painter.save();
painter.setClipPath(clip);
painter.fillRect(QRectF(0, 0, w, h), gradient);
painter.restore();
};
QLinearGradient left(0, 0, borderPx, 0);
left.setColorAt(0.0, edge);
left.setColorAt(1.0, inner);
drawSide(left, QPolygonF({ QPointF(0, 0), QPointF(borderPx, borderPx),
QPointF(borderPx, h - borderPx), QPointF(0, h) }));
QLinearGradient right(w, 0, w - borderPx, 0);
right.setColorAt(0.0, edge);
right.setColorAt(1.0, inner);
drawSide(right, QPolygonF({ QPointF(w, 0), QPointF(w - borderPx, borderPx),
QPointF(w - borderPx, h - borderPx), QPointF(w, h) }));
QLinearGradient top(0, 0, 0, borderPx);
top.setColorAt(0.0, edge);
top.setColorAt(1.0, inner);
drawSide(top, QPolygonF({ QPointF(0, 0), QPointF(w, 0),
QPointF(w - borderPx, borderPx), QPointF(borderPx, borderPx) }));
QLinearGradient bottom(0, h, 0, h - borderPx);
bottom.setColorAt(0.0, edge);
bottom.setColorAt(1.0, inner);
drawSide(bottom, QPolygonF({ QPointF(0, h), QPointF(w, h),
QPointF(w - borderPx, h - borderPx), QPointF(borderPx, h - borderPx) }));
painter.restore();
}
void GameWorldView::drawReplayOverlay(QPainter& painter) void GameWorldView::drawReplayOverlay(QPainter& painter)
{ {
if (!m_replayPlayer) { return; } if (!m_replayPlayer) { return; }
@@ -1932,7 +2042,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
} }
const QVector2D worldPos = widgetToWorld(event->pos()); const QVector2D worldPos = widgetToWorld(event->pos());
const entt::entity hitEntity = entityAtWorldPos(m_sim->admin(), worldPos); const entt::entity hitEntity = entityAtWorldPos(m_sim->getAdmin(), worldPos);
if (hitEntity != entt::null) if (hitEntity != entt::null)
{ {
@@ -1983,7 +2093,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds)); std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
} }
else if (const entt::entity scrapHit = else if (const entt::entity scrapHit =
scrapAtWorldPos(m_sim->admin(), worldPos); scrapHit != entt::null) scrapAtWorldPos(m_sim->getAdmin(), worldPos); scrapHit != entt::null)
{ {
// Scrap forms its own selection category; picking it clears any // Scrap forms its own selection category; picking it clears any
// building selection (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT). // building selection (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
@@ -2081,7 +2191,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
// (REQ-BLD-DEMOLISH, REQ-BLD-DEMOLISH-BOX). // (REQ-BLD-DEMOLISH, REQ-BLD-DEMOLISH-BOX).
for (BuildingId id : boxIds) for (BuildingId id : boxIds)
{ {
const Building* b = m_sim->buildings().findBuilding(id); const Building* b = m_sim->getBuildings().findBuilding(id);
if (b && b->type == BuildingType::Hq) { continue; } if (b && b->type == BuildingType::Hq) { continue; }
std::shared_ptr<DemolishCommand> command = std::shared_ptr<DemolishCommand> command =
std::make_shared<DemolishCommand>(); std::make_shared<DemolishCommand>();
@@ -2123,7 +2233,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
// No buildings in the box: a scrap-only box selects the scrap it covers // No buildings in the box: a scrap-only box selects the scrap it covers
// (REQ-UI-SCRAP-MULTI-SELECT). // (REQ-UI-SCRAP-MULTI-SELECT).
const std::vector<entt::entity> boxScrap = const std::vector<entt::entity> boxScrap =
scrapInBox(m_sim->admin(), m_boxStartTile, m_boxCurrentTile); scrapInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
if (!boxScrap.empty()) if (!boxScrap.empty())
{ {
if (!m_selectedBuildingIds.empty()) if (!m_selectedBuildingIds.empty())
@@ -2260,7 +2370,7 @@ void GameWorldView::pasteConfigTo(BuildingId id)
{ {
// Operational splitters are configured by tile; sites by BuildingId // Operational splitters are configured by tile; sites by BuildingId
// (mirrors SelectedBuildingPanel::onSplitterFilterChanged). // (mirrors SelectedBuildingPanel::onSplitterFilterChanged).
if (const Building* building = m_sim->buildings().findBuilding(id)) if (const Building* building = m_sim->getBuildings().findBuilding(id))
{ {
std::shared_ptr<SetSplitterFiltersCommand> command = std::shared_ptr<SetSplitterFiltersCommand> command =
std::make_shared<SetSplitterFiltersCommand>(); std::make_shared<SetSplitterFiltersCommand>();
@@ -2338,7 +2448,7 @@ void GameWorldView::exitBuilderMode()
std::make_shared<BuilderModeExitedEvent>()); std::make_shared<BuilderModeExitedEvent>());
} }
double GameWorldView::gameSpeed() const double GameWorldView::getGameSpeed() const
{ {
return m_gameSpeedMultiplier; return m_gameSpeedMultiplier;
} }
@@ -2391,6 +2501,12 @@ void GameWorldView::resetForNewGame()
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{})); std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
setGameSpeed(1.0); setGameSpeed(1.0);
// Rebase the wall-clock time source so a fresh run starts from a clean time
// base. Without this, wall time accumulated while a modal (Game Over, Win, or
// the escape menu) was open would be converted into ticks on the new run,
// fast-forwarding it by the time the player spent in the dialog.
m_frameTimer.restart();
m_tickDriver.reset();
update(); update();
} }
@@ -2402,17 +2518,17 @@ 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 tilePx()*0.2). // half a scrap pile's rendered radius (scrap is drawn at getTilePx()*0.2).
float maxRadius = 0.125f; float maxRadius = 0.125f;
if (m_sim->admin().isValid(event->target) if (m_sim->getAdmin().isValid(event->target)
&& m_sim->admin().hasAll<StationBodyComponent>(event->target)) && m_sim->getAdmin().hasAll<StationBodyComponent>(event->target))
{ {
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target); const StationBodyComponent& sb = m_sim->getAdmin().get<StationBodyComponent>(event->target);
const int shorter = std::min(sb.footprint.width(), sb.footprint.height()); const int shorter = std::min(sb.footprint.width(), sb.footprint.height());
maxRadius = shorter / 2.0f; maxRadius = shorter / 2.0f;
} }
else if (m_sim->admin().isValid(event->target) else if (m_sim->getAdmin().isValid(event->target)
&& m_sim->admin().hasAll<ScrapDataComponent>(event->target)) && m_sim->getAdmin().hasAll<ScrapDataComponent>(event->target))
{ {
maxRadius = 0.1f; maxRadius = 0.1f;
} }
@@ -2488,5 +2604,5 @@ bool GameWorldView::canAfford(BuildingType type) const
{ {
const BuildingDef* def = findBuildingDef(type); const BuildingDef* def = findBuildingDef(type);
if (!def) { return false; } if (!def) { return false; }
return m_sim->buildingBlocksStock() >= def->cost; return m_sim->getBuildingBlocksStock() >= def->cost;
} }

View File

@@ -76,7 +76,7 @@ public:
const ParsedReplay* replay, QWidget* parent = nullptr); const ParsedReplay* replay, QWidget* parent = nullptr);
~GameWorldView() override; ~GameWorldView() override;
double gameSpeed() const; double getGameSpeed() const;
bool isDebugDrawEnabled() const; bool isDebugDrawEnabled() const;
void resetFrameTimer(); void resetFrameTimer();
void setGameSpeed(double multiplier); void setGameSpeed(double multiplier);
@@ -131,25 +131,37 @@ private:
void drawBeams(QPainter& painter); void drawBeams(QPainter& painter);
void drawOverlays(QPainter& painter); void drawOverlays(QPainter& painter);
void drawScreenSpace(QPainter& painter); void drawScreenSpace(QPainter& painter);
// Vignette-style border shown while the game is paused (speed 0x, REQ-UI-PAUSE-BORDER)
// to make the paused state hard to miss: a black frame whose alpha fades from 50% at
// the viewport edges to 0% toward the center.
void drawPauseBorder(QPainter& painter);
// Vignette-style border shown while demolish mode is active (REQ-UI-DEMOLISH-BORDER),
// tinted with the demolish overlay color (including its configured alpha) from
// visuals.toml instead of black.
void drawDemolishBorder(QPainter& painter);
// Shared drawing for the pause/demolish vignettes: a mitred picture-frame border
// whose alpha fades from `edgeColor` (with its own alpha) at the viewport edge to
// fully transparent toward the center.
void drawVignetteBorder(QPainter& painter, const QColor& edgeColor);
void drawReplayOverlay(QPainter& painter); void drawReplayOverlay(QPainter& painter);
float tilePx() const; float getTilePx() const;
float viewportWidthTiles() const; float getViewportWidthTiles() const;
// World-X (tiles) at the left edge of the viewport. m_scrollXTiles stores the // World-X (tiles) at the left edge of the viewport. m_scrollXTiles stores the
// view center; this derives the left edge the world<->widget conversions need. // view center; this derives the left edge the world<->widget conversions need.
float viewLeftTiles() const; float getViewLeftTiles() const;
QPointF worldToWidget(QVector2D worldPos) const; QPointF worldToWidget(QVector2D worldPos) const;
QPointF tileToWidget(QPoint tile) const; QPointF tileToWidget(QPoint tile) const;
QPoint widgetToTile(QPoint widgetPt) const; QPoint widgetToTile(QPoint widgetPt) const;
QRectF tileRect(QPoint tile) const; QRectF tileRect(QPoint tile) const;
QRect viewportRect() const; QRect getViewportRect() const;
// Widget-space rectangle covering a building or construction site's footprint, // Widget-space rectangle covering a building or construction site's footprint,
// or nullopt if the id resolves to neither. Shared by the selection highlight // or nullopt if the id resolves to neither. Shared by the selection highlight
// and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK). // and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK).
std::optional<QRectF> footprintWidgetRect(BuildingId id) const; std::optional<QRectF> footprintWidgetRect(BuildingId id) const;
float asteroidLeftEdge() const; float getAsteroidLeftEdge() const;
float enemyStationRightEdge() const; float getEnemyStationRightEdge() const;
// Horizontal pan speed at a given view-center X, in tiles/s (REQ-UI-SCROLL-SPEED). // Horizontal pan speed at a given view-center X, in tiles/s (REQ-UI-SCROLL-SPEED).
float panSpeedTilesPerSecondAt(float viewCenterXTiles) const; float panSpeedTilesPerSecondAt(float viewCenterXTiles) const;
void clampScroll(); void clampScroll();
@@ -163,11 +175,13 @@ private:
std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const; std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const;
QVector2D widgetToWorld(QPoint widgetPt) const; QVector2D widgetToWorld(QPoint widgetPt) const;
void drawPortGlyph(QPainter& painter, QPoint bodyTile, void drawPortGlyph(QPainter& painter, QPoint tile,
Rotation direction, const QColor& color); Rotation direction, const QColor& color,
bool centered);
void drawBuildingGhost(QPainter& painter, BuildingType type, void drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation, bool valid); QPoint anchorTile, Rotation rotation, bool valid,
bool showPortTargetGlyphs);
void placeBlueprintAtTile(QPoint center); void placeBlueprintAtTile(QPoint center);
@@ -222,7 +236,7 @@ private:
std::mt19937 m_rng; std::mt19937 m_rng;
double m_gameSpeedMultiplier; double m_gameSpeedMultiplier;
double m_prevNonZeroSpeed; double m_prevNonZeroSpeed;
// World-X (tiles) at the center of the viewport (see viewLeftTiles()). // World-X (tiles) at the center of the viewport (see getViewLeftTiles()).
float m_scrollXTiles; float m_scrollXTiles;
QTimer* m_renderTimer; QTimer* m_renderTimer;

View File

@@ -32,6 +32,11 @@ HeaderBar::HeaderBar(const GameConfig* config, QWidget* parent)
QString::fromStdString(*config->world.buildingBlocksTooltip)); QString::fromStdString(*config->world.buildingBlocksTooltip));
} }
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this); m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
if (config->world.artifactTooltip)
{
m_artifactsLabel->setToolTip(
QString::fromStdString(*config->world.artifactTooltip));
}
m_bossLabel = new QLabel(tr("Boss Wave #1 Next boss: 5:00"), this); m_bossLabel = new QLabel(tr("Boss Wave #1 Next boss: 5:00"), this);
layout->addWidget(m_timeLabel); layout->addWidget(m_timeLabel);
layout->addWidget(m_blocksLabel); layout->addWidget(m_blocksLabel);

View File

@@ -14,7 +14,6 @@
#include "BlueprintPanel.h" #include "BlueprintPanel.h"
#include "BuildButtonGrid.h" #include "BuildButtonGrid.h"
#include "BuildingBlocksChangedEvent.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "Command.h" #include "Command.h"
#include "CommandRequestedEvent.h" #include "CommandRequestedEvent.h"
@@ -42,9 +41,9 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
setWindowTitle(tr("Dota Factory")); setWindowTitle(tr("Dota Factory"));
resize(1280, 768); resize(1280, 768);
m_headerBar = new HeaderBar(&sim->config(), this); m_headerBar = new HeaderBar(&sim->getConfig(), this);
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, m_configDir, m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
m_replay.get(), this); m_replay.get(), this);
m_sidePanel = new QWidget(this); m_sidePanel = new QWidget(this);
@@ -52,9 +51,9 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
sideLayout->setContentsMargins(1, 1, 1, 1); sideLayout->setContentsMargins(1, 1, 1, 1);
sideLayout->setSpacing(1); sideLayout->setSpacing(1);
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_sidePanel); m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_sidePanel); m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), m_sidePanel);
m_blueprintPanel = new BlueprintPanel(sim, &sim->config(), m_sidePanel); m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
sideLayout->addWidget(m_selectedBuildingPanel, 1); sideLayout->addWidget(m_selectedBuildingPanel, 1);
sideLayout->addWidget(m_buildButtonGrid, 1); sideLayout->addWidget(m_buildButtonGrid, 1);
@@ -151,18 +150,13 @@ void MainWindow::layoutPanels()
m_dimOverlay->setGeometry(0, 0, totalW, totalH); m_dimOverlay->setGeometry(0, 0, totalW, totalH);
} }
void MainWindow::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
{
m_buildButtonGrid->updateAffordability(event->blocks);
}
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event) void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
{ {
const double prevSpeed = m_gameWorldView->gameSpeed(); const double prevSpeed = m_gameWorldView->getGameSpeed();
m_gameWorldView->setGameSpeed(0.0); m_gameWorldView->setGameSpeed(0.0);
ModalDimScope dim(*m_dimOverlay); ModalDimScope dim(*m_dimOverlay);
SchematicChoiceDialog dialog(event->choices, m_sim->config().recipes, this); SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes, this);
dialog.exec(); dialog.exec();
std::shared_ptr<ApplySchematicChoiceCommand> command = std::shared_ptr<ApplySchematicChoiceCommand> command =
@@ -177,7 +171,7 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*event*/) void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*event*/)
{ {
const double prevSpeed = m_gameWorldView->gameSpeed(); const double prevSpeed = m_gameWorldView->getGameSpeed();
m_gameWorldView->setGameSpeed(0.0); m_gameWorldView->setGameSpeed(0.0);
ModalDimScope dim(*m_dimOverlay); ModalDimScope dim(*m_dimOverlay);
@@ -232,11 +226,11 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
const std::string& schematicId, const std::string& schematicId,
const ShipLayoutConfig& currentLayout) const ShipLayoutConfig& currentLayout)
{ {
const double prevSpeed = m_gameWorldView->gameSpeed(); const double prevSpeed = m_gameWorldView->getGameSpeed();
m_gameWorldView->setGameSpeed(0.0); m_gameWorldView->setGameSpeed(0.0);
std::set<std::string> unlockedModuleIds; std::set<std::string> unlockedModuleIds;
for (const ModuleDef& def : m_sim->config().modules.modules) for (const ModuleDef& def : m_sim->getConfig().modules.modules)
{ {
if (m_sim->isModuleSchematicUnlocked(def.id)) if (m_sim->isModuleSchematicUnlocked(def.id))
{ {
@@ -245,17 +239,17 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
} }
ModalDimScope dim(*m_dimOverlay); ModalDimScope dim(*m_dimOverlay);
ShipLayoutDialog dialog(&m_sim->config(), schematicId, currentLayout, ShipLayoutDialog dialog(&m_sim->getConfig(), schematicId, currentLayout,
m_layoutBlueprints, m_layoutBlueprints,
std::move(unlockedModuleIds), std::move(unlockedModuleIds),
m_gameWorldView->isDebugDrawEnabled(), m_gameWorldView->isDebugDrawEnabled(),
this); this);
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value()) if (dialog.exec() == QDialog::Accepted && dialog.getResult().has_value())
{ {
std::shared_ptr<SetShipLayoutCommand> command = std::shared_ptr<SetShipLayoutCommand> command =
std::make_shared<SetShipLayoutCommand>(); std::make_shared<SetShipLayoutCommand>();
command->id = shipyardId; command->id = shipyardId;
command->layout = *dialog.result(); command->layout = *dialog.getResult();
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<CommandRequestedEvent>(command)); std::make_shared<CommandRequestedEvent>(command));
} }
@@ -268,9 +262,9 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
{ {
// A construction site has no Building yet; fall back to its site record so // A construction site has no Building yet; fall back to its site record so
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG). // the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
const Building* b = m_sim->buildings().findBuilding(event->shipyardId); const Building* b = m_sim->getBuildings().findBuilding(event->shipyardId);
const ConstructionSite* s = const ConstructionSite* s =
b ? nullptr : m_sim->buildings().findSite(event->shipyardId); b ? nullptr : m_sim->getBuildings().findSite(event->shipyardId);
if (!b && !s) if (!b && !s)
{ {
return; return;
@@ -291,14 +285,14 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event)
{ {
const double prevSpeed = m_gameWorldView->gameSpeed(); const double prevSpeed = m_gameWorldView->getGameSpeed();
m_gameWorldView->setGameSpeed(0.0); m_gameWorldView->setGameSpeed(0.0);
// A construction site has no Building yet; fall back to its site record so // A construction site has no Building yet; fall back to its site record so
// the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG). // the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
const Building* b = m_sim->buildings().findBuilding(event->buildingId); const Building* b = m_sim->getBuildings().findBuilding(event->buildingId);
const ConstructionSite* s = const ConstructionSite* s =
b ? nullptr : m_sim->buildings().findSite(event->buildingId); b ? nullptr : m_sim->getBuildings().findSite(event->buildingId);
if (!b && !s) if (!b && !s)
{ {
m_gameWorldView->setGameSpeed(prevSpeed); m_gameWorldView->setGameSpeed(prevSpeed);
@@ -316,7 +310,7 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
// dereferenced after dialog.exec() returns. // dereferenced after dialog.exec() returns.
const std::string oldSchematic = b ? b->recipeId : s->recipeId; const std::string oldSchematic = b ? b->recipeId : s->recipeId;
const std::vector<RecipeSelectionOption> options = const std::vector<RecipeSelectionOption> options =
buildRecipeSelectionOptions(type, *m_sim, m_sim->config()); buildRecipeSelectionOptions(type, *m_sim, m_sim->getConfig());
const QString title = (type == BuildingType::Shipyard) const QString title = (type == BuildingType::Shipyard)
? tr("Select Schematic") ? tr("Select Schematic")
: tr("Select Recipe"); : tr("Select Recipe");
@@ -356,7 +350,7 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/) void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
{ {
const Tick tick = m_sim->currentTick(); const Tick tick = m_sim->getCurrentTick();
const int totalSeconds = static_cast<int>(ticksToSeconds(tick)); const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
const int minutes = totalSeconds / 60; const int minutes = totalSeconds / 60;
const int seconds = totalSeconds % 60; const int seconds = totalSeconds % 60;
@@ -403,7 +397,7 @@ void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/) void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/)
{ {
const Tick tick = m_sim->currentTick(); const Tick tick = m_sim->getCurrentTick();
const int totalSeconds = static_cast<int>(ticksToSeconds(tick)); const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
const int minutes = totalSeconds / 60; const int minutes = totalSeconds / 60;
const int seconds = totalSeconds % 60; const int seconds = totalSeconds % 60;

View File

@@ -6,7 +6,6 @@
#include <QWidget> #include <QWidget>
#include "BuildingBlocksChangedEvent.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "EscapeMenuRequestedEvent.h" #include "EscapeMenuRequestedEvent.h"
#include "EventHandler.h" #include "EventHandler.h"
@@ -32,8 +31,7 @@ class QCloseEvent;
class QResizeEvent; class QResizeEvent;
class MainWindow : public QWidget, class MainWindow : public QWidget,
public CombinedEventHandler<BuildingBlocksChangedEvent, public CombinedEventHandler<SchematicChoicesAvailableEvent,
SchematicChoicesAvailableEvent,
GameOverEvent, GameOverEvent,
WinEvent, WinEvent,
EscapeMenuRequestedEvent, EscapeMenuRequestedEvent,
@@ -52,7 +50,6 @@ protected:
void closeEvent(QCloseEvent* event) override; void closeEvent(QCloseEvent* event) override;
private: private:
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
void handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event) override; void handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event) override;
void handleEvent(std::shared_ptr<const GameOverEvent> event) override; void handleEvent(std::shared_ptr<const GameOverEvent> event) override;
void handleEvent(std::shared_ptr<const WinEvent> event) override; void handleEvent(std::shared_ptr<const WinEvent> event) override;

View File

@@ -274,8 +274,8 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
m_singleBuildingId = id; m_singleBuildingId = id;
hideAllWidgets(); hideAllWidgets();
const Building* b = m_sim->buildings().findBuilding(id); const Building* b = m_sim->getBuildings().findBuilding(id);
const ConstructionSite* s = b ? nullptr : m_sim->buildings().findSite(id); const ConstructionSite* s = b ? nullptr : m_sim->getBuildings().findSite(id);
if (!b && !s) if (!b && !s)
{ {
buildEmpty(); buildEmpty();
@@ -353,12 +353,12 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
std::optional<BeltSystem::SplitterInfo> info; std::optional<BeltSystem::SplitterInfo> info;
if (m_singleIsSite) if (m_singleIsSite)
{ {
info = m_sim->buildings().getSiteSplitterInfo(id); info = m_sim->getBuildings().getSiteSplitterInfo(id);
} }
else else
{ {
m_splitterTile = anchor; m_splitterTile = anchor;
info = m_sim->belts().getSplitterInfo(m_splitterTile); info = m_sim->getBelts().getSplitterInfo(m_splitterTile);
} }
buildSplitterFilters(info); buildSplitterFilters(info);
} }
@@ -397,7 +397,7 @@ void SelectedBuildingPanel::refreshSiteProgress(const ConstructionSite* s)
if (def && def->constructionTimeSeconds > 0) if (def && def->constructionTimeSeconds > 0)
{ {
const Tick duration = secondsToTicks(def->constructionTimeSeconds); const Tick duration = secondsToTicks(def->constructionTimeSeconds);
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration); const Tick elapsed = m_sim->getCurrentTick() - (s->completesAt - duration);
const int pct = static_cast<int>( const int pct = static_cast<int>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration); std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = tr("%1% complete").arg(pct); progress = tr("%1% complete").arg(pct);
@@ -554,7 +554,7 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
{ {
const Tick cycleTicks = secondsToTicks(durationSeconds); const Tick cycleTicks = secondsToTicks(durationSeconds);
const Tick completesAt = b->production->completesAt; const Tick completesAt = b->production->completesAt;
const Tick currentTick = m_sim->currentTick(); const Tick currentTick = m_sim->getCurrentTick();
const Tick elapsed = currentTick - (completesAt - cycleTicks); const Tick elapsed = currentTick - (completesAt - cycleTicks);
const int pct = static_cast<int>( const int pct = static_cast<int>(
std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks); std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks);
@@ -673,7 +673,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
} }
if (m_singleBuildingId == kInvalidBuildingId) { return; } if (m_singleBuildingId == kInvalidBuildingId) { return; }
const Building* b = m_sim->buildings().findBuilding(m_singleBuildingId); const Building* b = m_sim->getBuildings().findBuilding(m_singleBuildingId);
if (b) if (b)
{ {
if (m_titleLabel->text().startsWith(tr("(Building) "))) if (m_titleLabel->text().startsWith(tr("(Building) ")))
@@ -686,7 +686,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
} }
return; return;
} }
const ConstructionSite* s = m_sim->buildings().findSite(m_singleBuildingId); const ConstructionSite* s = m_sim->getBuildings().findSite(m_singleBuildingId);
if (s) if (s)
{ {
// A periodic tick only advances construction progress, so update just the // A periodic tick only advances construction progress, so update just the
@@ -720,13 +720,13 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
std::map<BuildingType, int> counts; std::map<BuildingType, int> counts;
for (BuildingId id : ids) for (BuildingId id : ids)
{ {
const Building* b = m_sim->buildings().findBuilding(id); const Building* b = m_sim->getBuildings().findBuilding(id);
if (b) if (b)
{ {
counts[b->type]++; counts[b->type]++;
continue; continue;
} }
const ConstructionSite* s = m_sim->buildings().findSite(id); const ConstructionSite* s = m_sim->getBuildings().findSite(id);
if (s) if (s)
{ {
counts[s->type]++; counts[s->type]++;
@@ -791,7 +791,7 @@ void SelectedBuildingPanel::buildSplitterFilters(
return; return;
} }
const std::vector<std::string> items = allItemIds(); const std::vector<std::string> items = getAllItemIds();
auto populateList = [&](QListWidget* list, QLabel* label, auto populateList = [&](QListWidget* list, QLabel* label,
const QString& dirLabel, const QString& dirLabel,
@@ -866,7 +866,7 @@ void SelectedBuildingPanel::onSplitterFilterChanged()
} }
} }
std::vector<std::string> SelectedBuildingPanel::allItemIds() const std::vector<std::string> SelectedBuildingPanel::getAllItemIds() const
{ {
std::set<std::string> seen; std::set<std::string> seen;
for (const RecipeDef& recipe : m_config->recipes.recipes) for (const RecipeDef& recipe : m_config->recipes.recipes)
@@ -888,7 +888,7 @@ void SelectedBuildingPanel::onClearBelt()
std::vector<QPoint> tiles; std::vector<QPoint> tiles;
for (BuildingId id : m_selectedBuildingIds) for (BuildingId id : m_selectedBuildingIds)
{ {
const Building* b = m_sim->buildings().findBuilding(id); const Building* b = m_sim->getBuildings().findBuilding(id);
if (b && isBeltLike(b->type)) if (b && isBeltLike(b->type))
{ {
for (const QPoint& cell : b->bodyCells) for (const QPoint& cell : b->bodyCells)
@@ -918,7 +918,7 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEven
m_scrapLabel->hide(); m_scrapLabel->hide();
clearContent(); clearContent();
EntityAdmin& admin = m_sim->admin(); EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity; entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity)) if (!admin.isValid(entity))
@@ -944,7 +944,7 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEven
void SelectedBuildingPanel::buildEntityShip(entt::entity entity) void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
{ {
EntityAdmin& admin = m_sim->admin(); EntityAdmin& admin = m_sim->getAdmin();
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity); const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
const HealthComponent& health = admin.get<HealthComponent>(entity); const HealthComponent& health = admin.get<HealthComponent>(entity);
@@ -976,7 +976,7 @@ void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
void SelectedBuildingPanel::buildEntityStation(entt::entity entity) void SelectedBuildingPanel::buildEntityStation(entt::entity entity)
{ {
EntityAdmin& admin = m_sim->admin(); EntityAdmin& admin = m_sim->getAdmin();
const HealthComponent& health = admin.get<HealthComponent>(entity); const HealthComponent& health = admin.get<HealthComponent>(entity);
const bool isEnemy = admin.hasAll<FactionComponent>(entity) const bool isEnemy = admin.hasAll<FactionComponent>(entity)
@@ -1019,7 +1019,7 @@ void SelectedBuildingPanel::refreshEntityStats()
{ {
if (!m_selectedEntity.has_value()) { return; } if (!m_selectedEntity.has_value()) { return; }
EntityAdmin& admin = m_sim->admin(); EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity; entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity)) if (!admin.isValid(entity))
@@ -1098,7 +1098,7 @@ void SelectedBuildingPanel::refreshScrapTotal()
{ {
// Sum the remaining amounts of the still-living selected piles (REQ-UI-SCRAP-PANEL). // Sum the remaining amounts of the still-living selected piles (REQ-UI-SCRAP-PANEL).
int total = 0; int total = 0;
for (const ScrapInfo& info : m_sim->scraps().allScrapInfo()) for (const ScrapInfo& info : m_sim->getScraps().getAllScrapInfo())
{ {
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity) if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity)
!= m_selectedScrap.end()) != m_selectedScrap.end())

View File

@@ -90,7 +90,7 @@ private:
void buildSplitterFilters(const std::optional<BeltSystem::SplitterInfo>& info); void buildSplitterFilters(const std::optional<BeltSystem::SplitterInfo>& info);
const RecipeDef* findRecipe(const Building* b) const; const RecipeDef* findRecipe(const Building* b) const;
const ShipDef* findShipDef(const std::string& id) const; const ShipDef* findShipDef(const std::string& id) const;
std::vector<std::string> allItemIds() const; std::vector<std::string> getAllItemIds() const;
Simulation* m_sim; Simulation* m_sim;
const GameConfig* m_config; const GameConfig* m_config;

View File

@@ -578,7 +578,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
}); });
} }
std::optional<ShipLayoutConfig> ShipLayoutDialog::result() const std::optional<ShipLayoutConfig> ShipLayoutDialog::getResult() const
{ {
return m_result; return m_result;
} }

View File

@@ -30,7 +30,7 @@ public:
bool debugDraw, bool debugDraw,
QWidget* parent = nullptr); QWidget* parent = nullptr);
std::optional<ShipLayoutConfig> result() const; std::optional<ShipLayoutConfig> getResult() const;
protected: protected:
void keyPressEvent(QKeyEvent* event) override; void keyPressEvent(QKeyEvent* event) override;