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
orbit_factor = 0.8
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]
asteroid_width_tiles = 60

View File

@@ -11,6 +11,7 @@ departure_interval_seconds = 20
orbit_factor = 0.8
rally_orbit_radius_tiles = 5.0
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]
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`).
### 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
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:
- **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).
- **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).
@@ -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-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-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-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).
@@ -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.
- 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-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-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
- 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.
- **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×).
- **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.)
@@ -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-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-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-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-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-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();
}
std::string ArenaStatus::TeamStatus::ehpPercentText() const
std::string ArenaStatus::TeamStatus::getEhpPercentText() const
{
if (maxEhp <= 0.0)
{
@@ -312,7 +312,7 @@ void ArenaSimulation::requestStop()
m_stopRequested.store(true, std::memory_order_relaxed);
}
ArenaStatus ArenaSimulation::status() const
ArenaStatus ArenaSimulation::getStatus() const
{
std::lock_guard<std::mutex> lock(m_statusMutex);
return m_status;
@@ -473,42 +473,42 @@ bool ArenaSimulation::isFinished() const
return m_finished;
}
int ArenaSimulation::winnerTeam() const
int ArenaSimulation::getWinnerTeam() const
{
return m_winnerTeam;
}
Tick ArenaSimulation::currentTick() const
Tick ArenaSimulation::getCurrentTick() const
{
return m_currentTick;
}
const ArenaConfig& ArenaSimulation::arenaConfig() const
const ArenaConfig& ArenaSimulation::getArenaConfig() const
{
return m_arenaConfig;
}
const BuildingSystem& ArenaSimulation::buildings() const
const BuildingSystem& ArenaSimulation::getBuildings() const
{
return *m_buildingSystem;
}
const ShipSystem& ArenaSimulation::ships() const
const ShipSystem& ArenaSimulation::getShips() const
{
return *m_shipSystem;
}
const ScrapSystem& ArenaSimulation::scraps() const
const ScrapSystem& ArenaSimulation::getScraps() const
{
return *m_scrapSystem;
}
EntityAdmin& ArenaSimulation::admin()
EntityAdmin& ArenaSimulation::getAdmin()
{
return m_admin;
}
const EntityAdmin& ArenaSimulation::admin() const
const EntityAdmin& ArenaSimulation::getAdmin() const
{
return m_admin;
}

View File

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

View File

@@ -73,7 +73,7 @@ void ArenaView::setGameSpeed(double multiplier)
std::make_shared<GameSpeedChangedEvent>(multiplier));
}
double ArenaView::gameSpeed() const
double ArenaView::getGameSpeed() const
{
return m_gameSpeedMultiplier;
}
@@ -121,7 +121,7 @@ void ArenaView::onFrame()
// Expire old beams. Lifetime is measured in game ticks so beams stay
// 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;
for (const ActiveBeam& b : m_activeBeams)
{
@@ -144,16 +144,16 @@ void ArenaView::onFrame()
void ArenaView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
{
float maxRadius = 0.125f;
if (m_sim->admin().isValid(event->target)
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
if (m_sim->getAdmin().isValid(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());
maxRadius = shorter / 2.0f;
}
else if (m_sim->admin().isValid(event->target)
&& m_sim->admin().hasAll<ScrapDataComponent>(event->target))
else if (m_sim->getAdmin().isValid(event->target)
&& m_sim->getAdmin().hasAll<ScrapDataComponent>(event->target))
{
maxRadius = 0.1f;
}
@@ -192,9 +192,9 @@ void ArenaView::paintGL()
// 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
+ ac.contestZoneWidth_tiles
+ ac.enemyBufferWidth_tiles;
@@ -209,8 +209,8 @@ float ArenaView::tilePx() const
QPointF ArenaView::worldToWidget(QVector2D worldPos) const
{
return QPointF(
static_cast<qreal>(worldPos.x() * tilePx()),
static_cast<qreal>(worldPos.y() * tilePx()));
static_cast<qreal>(worldPos.x() * getTilePx()),
static_cast<qreal>(worldPos.y() * getTilePx()));
}
QPointF ArenaView::tileToWidget(QPoint tile) const
@@ -223,21 +223,21 @@ QRectF ArenaView::tileRect(QPoint tile) const
{
const QPointF tl = tileToWidget(tile);
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
{
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 m_sim->admin().get<PositionComponent>(entity).value;
return m_sim->getAdmin().get<PositionComponent>(entity).value;
}
QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const
{
const float px = tilePx();
const float px = getTilePx();
if (px < 0.001f) { return QVector2D(0.0f, 0.0f); }
return QVector2D(static_cast<float>(widgetPt.x()) / px,
static_cast<float>(widgetPt.y()) / px);
@@ -248,7 +248,7 @@ void ArenaView::mousePressEvent(QMouseEvent* event)
if (event->button() == Qt::LeftButton)
{
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)
{
@@ -282,7 +282,7 @@ void ArenaView::keyPressEvent(QKeyEvent* event)
void ArenaView::drawTiles(QPainter& painter)
{
const ArenaConfig& ac = m_sim->arenaConfig();
const ArenaConfig& ac = m_sim->getArenaConfig();
const int totalWidth = ac.playerBufferWidth_tiles
+ ac.contestZoneWidth_tiles
+ ac.enemyBufferWidth_tiles;
@@ -300,7 +300,7 @@ void ArenaView::drawTiles(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 =
m_visuals->buildings.find(b.type);
@@ -315,8 +315,8 @@ void ArenaView::drawBuildings(QPainter& painter)
const QPointF tl = tileToWidget(b.anchor);
const QRectF bboxRect(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(tilePx()),
b.footprint.height() * static_cast<qreal>(tilePx()));
b.footprint.width() * static_cast<qreal>(getTilePx()),
b.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush);
@@ -332,8 +332,8 @@ void ArenaView::drawBuildings(QPainter& painter)
void ArenaView::drawScrap(QPainter& painter)
{
const float r = tilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo())
const float r = getTilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo())
{
const QPointF center = worldToWidget(scrap.position);
painter.setBrush(QColor(128, 110, 90));
@@ -345,7 +345,7 @@ void ArenaView::drawScrap(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)
{
const BuildingType visType = f.isEnemy
@@ -364,8 +364,8 @@ void ArenaView::drawStations(QPainter& painter)
const QPointF tl = tileToWidget(sb.anchor);
const QRectF bboxRect(tl.x(), tl.y(),
sb.footprint.width() * static_cast<qreal>(tilePx()),
sb.footprint.height() * static_cast<qreal>(tilePx()));
sb.footprint.width() * static_cast<qreal>(getTilePx()),
sb.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush);
@@ -374,7 +374,7 @@ void ArenaView::drawStations(QPainter& painter)
if (h.maxHp > 0.0f)
{
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 barW = bboxRect.width();
painter.fillRect(QRectF(bboxRect.left(), barY, barW, barH),
@@ -394,7 +394,7 @@ void ArenaView::drawStations(QPainter& painter)
void ArenaView::drawShips(QPainter& painter)
{
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>(
[&](entt::entity e, const ShipIdentityComponent& si,
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 perp(-dir.y(), dir.x());
const float fwd = tilePx() * 0.45f;
const float side = tilePx() * 0.25f;
const float fwd = getTilePx() * 0.45f;
const float side = getTilePx() * 0.25f;
QPolygonF tri;
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 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 barY = center.y() + static_cast<qreal>(fwd) + 1.0;
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)
{
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.setBrush(Qt::NoBrush);
painter.drawEllipse(center, radius, radius);
@@ -448,7 +448,7 @@ void ArenaView::drawShips(QPainter& painter)
void ArenaView::drawDebugSensorRanges(QPainter& painter)
{
painter.setBrush(Qt::NoBrush);
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, SensorRangeComponent>(
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const SensorRangeComponent& sensor)
{
@@ -458,7 +458,7 @@ void ArenaView::drawDebugSensorRanges(QPainter& painter)
const QPointF center = worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(tilePx());
* static_cast<qreal>(getTilePx());
QColor circleColor = it->second.outline;
circleColor.setAlpha(77);
painter.setPen(QPen(circleColor, 1));
@@ -487,7 +487,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
painter.drawLine(worldToWidget(from), worldToWidget(to));
};
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent,
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, AttackBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac,
@@ -502,7 +502,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
});
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent,
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, RepairBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac,
@@ -517,7 +517,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
});
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent,
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, SalvageScrapBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac,

View File

@@ -32,7 +32,7 @@ public:
~ArenaView() override;
void setGameSpeed(double multiplier);
double gameSpeed() const;
double getGameSpeed() const;
void togglePause();
void stopRendering();
@@ -56,7 +56,7 @@ private:
void drawDebugTargetLines(QPainter& painter);
void drawBeams(QPainter& painter);
float tilePx() const;
float getTilePx() const;
QPointF worldToWidget(QVector2D worldPos) const;
QPointF tileToWidget(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)));
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.ehpPercentText())));
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.getEhpPercentText())));
QString lines;
for (const ArenaStatus::Entry& entry : team.entries)

View File

@@ -46,7 +46,7 @@ namespace
header = QStringLiteral("[WON] ") + header;
}
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);
}
@@ -147,7 +147,7 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
contentLayout->addWidget(entry.widget);
entry.widget->updateStatus(entry.simulation->status());
entry.widget->updateStatus(entry.simulation->getStatus());
m_arenas.push_back(std::move(entry));
}
@@ -179,14 +179,14 @@ void BalancingWindow::pollStatuses()
{
if (entry.worker.joinable())
{
const ArenaStatus status = entry.simulation->status();
const ArenaStatus status = entry.simulation->getStatus();
entry.widget->updateStatus(status);
}
}
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);
}
@@ -242,7 +242,7 @@ void BalancingWindow::startArena(int index)
entry.simulation = std::make_unique<ArenaSimulation>(
m_gameConfig, entry.config, m_nextSeed++);
entry.widget->startSimulation();
entry.widget->updateStatus(entry.simulation->status());
entry.widget->updateStatus(entry.simulation->getStatus());
ArenaSimulation* sim = entry.simulation.get();
entry.worker = std::thread([sim]() { sim->run(); });
updateButtons();
@@ -278,7 +278,7 @@ void BalancingWindow::inspectArena(int index)
entry.widget->resetToGrey();
entry.widget->startSimulation();
entry.widget->updateStatus(m_inspectedSim->status());
entry.widget->updateStatus(m_inspectedSim->getStatus());
m_inspectWindow = new InspectWindow(
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
@@ -336,7 +336,7 @@ void BalancingWindow::updateButtons()
bool allRunning = true;
for (ArenaEntry& entry : m_arenas)
{
if (entry.worker.joinable() && !entry.simulation->status().finished)
if (entry.worker.joinable() && !entry.simulation->getStatus().finished)
{
anyRunning = true;
}

View File

@@ -199,7 +199,7 @@ void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> eve
void InspectWindow::pollStatus()
{
const ArenaStatus status = m_sim->status();
const ArenaStatus status = m_sim->getStatus();
updateInfoPanel(status);
refreshEntityStats();
}
@@ -228,7 +228,7 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
}
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;
for (const ArenaStatus::Entry& entry : team.entries)
@@ -256,7 +256,7 @@ void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event
{
m_selectedEntity = event->entity;
EntityAdmin& admin = m_sim->admin();
EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity))
@@ -332,7 +332,7 @@ void InspectWindow::refreshEntityStats()
{
if (!m_selectedEntity.has_value()) { return; }
EntityAdmin& admin = m_sim->admin();
EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity))

View File

@@ -279,6 +279,12 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
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.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"));

View File

@@ -30,7 +30,7 @@ public:
// Evaluates the expression at the given x. Requires a compiled formula.
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; }
private:

View File

@@ -82,6 +82,10 @@ struct WorldConfig
// (REQ-UI-BLOCKS-TOOLTIP). Presentation-only; the simulation ignores it.
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;
WorldExpansion expansion;
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).
applyPendingCollections(currentTick, scraps);
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
const std::vector<ScrapInfo> allScrap = scraps.getAllScrapInfo();
// Tick down per-module collection cooldowns.
m_admin.forEach<SalvagerComponent>(

View File

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

View File

@@ -35,7 +35,7 @@ public:
bool collectOne(entt::entity entity);
// Lightweight snapshot for callers that need to iterate all scrap.
std::vector<ScrapInfo> allScrapInfo() const;
std::vector<ScrapInfo> getAllScrapInfo() const;
private:
EntityAdmin& m_admin;

View File

@@ -19,7 +19,7 @@ void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const ScrapSystem& scra
{
TRACE();
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>(
[&](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
// output-buffer capacity rule (REQ-MAT-OUTPUT-BUFFER) counts emerging items,
// since they have not yet left the building.
int outputItemCount() const
int getOutputItemCount() const
{
int count = static_cast<int>(outputBuffer.items.size());
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).
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
{
const Building* building = sim.buildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.buildings().findSite(id);
const Building* building = sim.getBuildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
if (!building && !site)
{
return std::nullopt;
}
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)
{
return std::nullopt;
@@ -52,8 +52,8 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
{
const Building* building = sim.buildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.buildings().findSite(id);
const Building* building = sim.getBuildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
if (!building && !site)
{
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.
const std::optional<BeltSystem::SplitterInfo> info =
sim.belts().getSplitterInfo(building->anchor);
sim.getBelts().getSplitterInfo(building->anchor);
if (info.has_value())
{
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
// count against the buffer (REQ-MAT-OUTPUT-EMERGE).
const int newSize = building.outputItemCount()
const int newSize = building.getOutputItemCount()
+ static_cast<int>(chosen.size());
if (newSize > building.outputBuffer.capacity)
{
@@ -1279,12 +1279,12 @@ const ConstructionSite* BuildingSystem::findSite(BuildingId id) const
return nullptr;
}
std::vector<Building> BuildingSystem::allBuildings() const
std::vector<Building> BuildingSystem::getAllBuildings() const
{
return m_buildings;
}
std::vector<ConstructionSite> BuildingSystem::allSites() const
std::vector<ConstructionSite> BuildingSystem::getAllSites() const
{
return std::vector<ConstructionSite>(m_constructionQueue.begin(),
m_constructionQueue.end());
@@ -1308,7 +1308,7 @@ bool isProductionBuildingType(BuildingType type)
}
} // namespace
int BuildingSystem::productionBuildingCount() const
int BuildingSystem::getProductionBuildingCount() const
{
int count = 0;
for (const Building& b : m_buildings)
@@ -1318,7 +1318,7 @@ int BuildingSystem::productionBuildingCount() const
return count;
}
int BuildingSystem::activeProductionBuildingCount() const
int BuildingSystem::getActiveProductionBuildingCount() const
{
int count = 0;
for (const Building& b : m_buildings)
@@ -1328,7 +1328,7 @@ int BuildingSystem::activeProductionBuildingCount() const
return count;
}
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::getAllBeltTiles() const
{
std::vector<BeltTileInfo> result;
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
// (REQ-MAT-OUTPUT-EMERGE).
if (bay->outputItemCount() >= bay->outputBuffer.capacity)
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
{
return false;
}

View File

@@ -112,17 +112,17 @@ public:
const Building* findBuilding(BuildingId id) const;
const ConstructionSite* findSite(BuildingId id) const;
std::vector<Building> allBuildings() const;
std::vector<ConstructionSite> allSites() const;
std::vector<Building> getAllBuildings() const;
std::vector<ConstructionSite> getAllSites() const;
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
int productionBuildingCount() const;
int getProductionBuildingCount() const;
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
// that currently has an active production cycle.
int activeProductionBuildingCount() const;
std::vector<BeltTileInfo> allBeltTiles() const;
int getActiveProductionBuildingCount() const;
std::vector<BeltTileInfo> getAllBeltTiles() const;
bool isTileOccupied(QPoint tile) const;
// 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.
m_recorder->startNewRun(m_simulation.getSeed(),
m_simulation.rngFingerprint());
m_simulation.getRngFingerprint());
}
}
else
{
// Commands drain before the tick batch, so currentTick is the count of
// completed ticks the command is pinned to.
const Tick tick = m_simulation.currentTick();
const Tick tick = m_simulation.getCurrentTick();
m_simulation.apply(*command);
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);
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()
{
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);
}
else if (m_simulation.rngFingerprint() != entry.fingerprint)
else if (m_simulation.getRngFingerprint() != entry.fingerprint)
{
m_desyncTick = tick;
}

View File

@@ -19,7 +19,7 @@ class Simulation;
// each frame, for each tick to run:
// if (player.isFinished()) break;
// sim.tick();
// player.advanceTo(sim.currentTick());
// player.advanceTo(sim.getCurrentTick());
class ReplayPlayer
{
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()));
}
}
return toHex(hasher.value());
return toHex(hasher.getValue());
}
void ReplayRecorder::startNewRun(unsigned int seed, std::uint64_t initialRngFingerprint)
@@ -124,7 +124,7 @@ bool ReplayRecorder::isOpen() const
return m_stream.is_open();
}
const std::string& ReplayRecorder::currentFilePath() const
const std::string& ReplayRecorder::getCurrentFilePath() const
{
return m_filePath;
}

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ public:
explicit Simulation(GameConfig config, unsigned int seed = 0);
~Simulation();
const GameConfig& config() const;
const GameConfig& getConfig() const;
// Reinitializes all simulation state as if constructed fresh.
void reset(unsigned int seed = 0);
@@ -68,26 +68,26 @@ public:
// Returns true if there are pending schematic choices waiting for player input.
bool hasSchematicChoicesPending() const;
Tick currentTick() const;
Tick getCurrentTick() const;
// The seed this run was (re)initialized with; written to the replay header.
unsigned int getSeed() const;
int buildingBlocksStock() const;
int getBuildingBlocksStock() const;
// Current asteroid width in tiles = base width + purchased expansions
// (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
// (REQ-EXP-COST); x = number of expansions already purchased.
int currentExpansionCost() const;
int getCurrentExpansionCost() const;
bool isGameOver() const;
bool isWon() const;
int artifactCount() const;
double threatLevel() const;
double threatAccumulationRate() const;
double maxFactoryProductionThreatRate() const;
double currentFactoryProductionThreatRate() const;
int bossWaveCounter() const;
Tick bossCountdownTicks() const;
Tick normalGapRemainingTicks() const;
int getArtifactCount() const;
double getThreatLevel() const;
double getThreatAccumulationRate() const;
double getMaxFactoryProductionThreatRate() const;
double getCurrentFactoryProductionThreatRate() const;
int getBossWaveCounter() const;
Tick getBossCountdownTicks() const;
Tick getNormalGapRemainingTicks() const;
// Ship schematic state query.
bool isSchematicUnlocked(const std::string& shipId) const;
@@ -102,25 +102,25 @@ public:
// -- Determinism (see docs/replay_design.md) -----------------------------
// 64-bit fingerprint of the RNG stream state. Cheap; written to the replay
// 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,
// 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;
// Const subsystem accessors (queries only). The mutable counterparts are
// private and reachable only through Simulation::apply (the command
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot
// mutate the factory outside the recorded command path (docs/replay_design.md).
const BuildingSystem& buildings() const;
const BeltSystem& belts() const;
ShipSystem& ships();
const ShipSystem& ships() const;
ScrapSystem& scraps();
const ScrapSystem& scraps() const;
EntityAdmin& admin();
const EntityAdmin& admin() const;
const BuildingSystem& getBuildings() const;
const BeltSystem& getBelts() const;
ShipSystem& getShips();
const ShipSystem& getShips() const;
ScrapSystem& getScraps();
const ScrapSystem& getScraps() const;
EntityAdmin& getAdmin();
const EntityAdmin& getAdmin() const;
private:
// Grants tests access to the private player-action mutators below without
@@ -148,8 +148,8 @@ private:
void tryExpandAsteroid();
// Mutable subsystem accessors; same chokepoint rule as the mutators above.
BuildingSystem& buildingsMutable();
BeltSystem& beltsMutable();
BuildingSystem& getBuildingsMutable();
BeltSystem& getBeltsMutable();
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
Hasher hasher;
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 std::string& text);
std::uint64_t value() const { return m_state; }
std::uint64_t getValue() const { return m_state; }
private:
std::uint64_t m_state = 14695981039346656037ull; // FNV-1a 64-bit offset basis

View File

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

View File

@@ -34,26 +34,26 @@ public:
// (REQ-WAV-BOSS-ADVANCE, REQ-PSH-STATION-STATS).
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
// the rate formula evaluates to a negative value.
double threatAccumulationRate() const;
double getThreatAccumulationRate() const;
// Current enemy-station generation level (0 for initial set,
// 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.
int bossWaveCounter() const;
int getBossWaveCounter() const;
// 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).
// Frozen during quiet windows.
Tick normalGapRemainingTicks() const;
Tick getNormalGapRemainingTicks() const;
private:
struct SpawnEntry

View File

@@ -19,7 +19,7 @@ static GameConfig loadConfig()
static void killEnemyStations(Simulation& sim)
{
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health)
{
if (faction.isEnemy)
@@ -65,7 +65,7 @@ TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game
"[artifact_win]")
{
const Simulation sim(loadConfig());
CHECK(sim.artifactCount() == 0);
CHECK(sim.getArtifactCount() == 0);
CHECK_FALSE(sim.isWon());
}
@@ -145,7 +145,7 @@ TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
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",
@@ -166,7 +166,7 @@ TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increm
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());
SimulationTestAccess::applySchematicChoice(sim,findArtifactChoiceIndex(sim));
CHECK(sim.artifactCount() == 1);
CHECK(sim.getArtifactCount() == 1);
CHECK_FALSE(sim.isWon());
}
@@ -226,7 +226,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required nu
SimulationTestAccess::applySchematicChoice(sim,index);
}
CHECK(sim.artifactCount() == 2);
CHECK(sim.getArtifactCount() == 2);
CHECK(sim.isWon());
}
@@ -249,6 +249,6 @@ TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state",
sim.reset();
CHECK(sim.artifactCount() == 0);
CHECK(sim.getArtifactCount() == 0);
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(idB != kInvalidBuildingId);
REQUIRE(sim.buildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
REQUIRE(sim.buildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
REQUIRE_FALSE(sim.buildings().isTileOccupied(cursor)); // center not occupied
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(cursor)); // center not occupied
}
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).
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; }
}
REQUIRE(beltCost > 0);
const int startBlocks = sim.buildingBlocksStock();
const int startBlocks = sim.getBuildingBlocksStock();
REQUIRE(startBlocks >= 2 * beltCost); // test config has enough starting blocks
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);
REQUIRE(sim.buildingBlocksStock() == startBlocks - 2 * beltCost);
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - 2 * beltCost);
}
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.
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; }
}
@@ -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.
// Non-overlapping columns: miner body is 2 wide, so step by 2.
int col = -2;
while (sim.buildingBlocksStock() >= minerCost)
while (sim.getBuildingBlocksStock() >= minerCost)
{
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(col, 0), Rotation::East);
col -= 2;
}
const int blocksBeforeAttempt = sim.buildingBlocksStock();
const int blocksBeforeAttempt = sim.getBuildingBlocksStock();
const BuildingId id = SimulationTestAccess::place(sim,
BuildingType::Miner, QPoint(col - 2, 0), Rotation::East);
// Placement must fail and leave the stock unchanged.
REQUIRE(id == kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == blocksBeforeAttempt);
REQUIRE(sim.getBuildingBlocksStock() == blocksBeforeAttempt);
}
TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing",
"[blueprint]")
{
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
// 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);
REQUIRE(id == kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == startBlocks);
REQUIRE(sim.buildings().allSites().empty());
REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
REQUIRE(sim.getBuildings().getAllSites().empty());
}
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());
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; }
}
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
// (-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);
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == startBlocks - minerCost);
REQUIRE(sim.buildings().isTileOccupied(QPoint(-3, 0)));
REQUIRE(sim.buildings().isTileOccupied(QPoint(-2, 0)));
REQUIRE(sim.buildings().isTileOccupied(QPoint(-3, 1)));
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 0)));
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-2, 0)));
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 1)));
// 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");
const ConstructionSite* site = sim.buildings().findSite(id);
const ConstructionSite* site = sim.getBuildings().findSite(id);
REQUIRE(site != nullptr);
REQUIRE(site->recipeId == "mine_iron_ore");
}
@@ -692,7 +692,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
sim.tick();
}
const Building* b = sim.buildings().findBuilding(id);
const Building* b = sim.getBuildings().findBuilding(id);
REQUIRE(b != nullptr);
REQUIRE(b->recipeId == "mine_copper_ore");
}
@@ -712,8 +712,8 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildings().findSite(id) != nullptr);
REQUIRE(sim.buildings().findBuilding(id) == nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
@@ -748,14 +748,14 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
REQUIRE(idA != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
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.
const BuildingId idB =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East);
REQUIRE(idB != kInvalidBuildingId);
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 });
@@ -779,7 +779,7 @@ TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildings().findSite(id) != nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
}
@@ -859,7 +859,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores 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->shipLayout.has_value());
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.
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; }
}
@@ -896,7 +896,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
sim.tick();
}
const Building* b = sim.buildings().findBuilding(id);
const Building* b = sim.getBuildings().findBuilding(id);
REQUIRE(b != nullptr);
REQUIRE(b->shipLayout.has_value());
REQUIRE(b->shipLayout->placedModules.size() == 1);

View File

@@ -114,8 +114,8 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildings().findBuilding(id) == nullptr);
REQUIRE(sim.buildings().findSite(id) != nullptr);
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
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.
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId);
REQUIRE(f.bs.allSites().empty());
REQUIRE(f.bs.getAllSites().empty());
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,
QPoint(0, heightTiles - 1), Rotation::East, 0);
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]")
@@ -165,7 +165,7 @@ TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge",
const BuildingId id = f.bs.place(BuildingType::Miner,
QPoint(leftEdgeX - 1, 0), Rotation::East, 0);
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",
@@ -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);
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East));
REQUIRE(bs.allBuildings().size() == 1);
REQUIRE(bs.allBuildings()[0].type == BuildingType::Belt);
REQUIRE(bs.allBuildings()[0].anchor == QPoint(5, 5));
REQUIRE(bs.getAllBuildings().size() == 1);
REQUIRE(bs.getAllBuildings()[0].type == BuildingType::Belt);
REQUIRE(bs.getAllBuildings()[0].anchor == QPoint(5, 5));
}
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);
REQUIRE(bs.allSites().size() == 1);
REQUIRE(bs.allBuildings().empty());
REQUIRE(bs.getAllSites().size() == 1);
REQUIRE(bs.getAllBuildings().empty());
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.
REQUIRE(refund == 15 * cfg.world.refundPercentage / 100);
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);
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]")
@@ -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(5, 5), Rotation::East, 0);
REQUIRE(bs.allSites().size() == 2);
REQUIRE(bs.allSites()[0].completesAt > 0);
REQUIRE(bs.allSites()[1].completesAt == 0);
REQUIRE(bs.getAllSites().size() == 2);
REQUIRE(bs.getAllSites()[0].completesAt > 0);
REQUIRE(bs.getAllSites()[1].completesAt == 0);
}
TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]")
@@ -358,7 +358,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.allSites().empty());
REQUIRE(bs.getAllSites().empty());
REQUIRE(bs.findBuilding(id) != nullptr);
}
@@ -383,9 +383,9 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.allSites().size() == 1);
REQUIRE(bs.allSites().front().id == id2);
REQUIRE(bs.allSites().front().completesAt > 0);
REQUIRE(bs.getAllSites().size() == 1);
REQUIRE(bs.getAllSites().front().id == id2);
REQUIRE(bs.getAllSites().front().completesAt > 0);
}
// ---------------------------------------------------------------------------
@@ -457,7 +457,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
REQUIRE(b != nullptr);
// Both produced items are held on the output side (buffer + emerging lane),
// 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());
}
@@ -485,23 +485,23 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
Tick tick = 0;
// 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 smelter (15s) starts and completes at tick 300 + 450 = 750.
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);
REQUIRE(bs.productionBuildingCount() == 2);
REQUIRE(bs.getProductionBuildingCount() == 2);
// Neither is producing yet: the miner has no recipe selected, and the
// 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");
runTicks(bs, belts, 1, tick);
REQUIRE(bs.activeProductionBuildingCount() == 1);
REQUIRE(bs.getActiveProductionBuildingCount() == 1);
}
TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state",
@@ -524,11 +524,11 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
Tick tick = 0;
// 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).
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
// 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);
REQUIRE(b != nullptr);
REQUIRE(b->outputItemCount() == 2);
REQUIRE(b->getOutputItemCount() == 2);
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);
// Nothing was delivered, and the producer's output side has backed up to its cap.
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);
REQUIRE(b != nullptr);
REQUIRE(b->outputItemCount() > 0);
REQUIRE(b->getOutputItemCount() > 0);
}
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);
// Clearing the output buffer on a recipe change also discards emerging items
// (REQ-MAT-OUTPUT-EMERGE).
REQUIRE(b->outputItemCount() == 0);
REQUIRE(b->getOutputItemCount() == 0);
REQUIRE_FALSE(b->production.has_value());
}
@@ -1083,7 +1083,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(bs.allSites().empty());
REQUIRE(bs.getAllSites().empty());
const std::optional<BuildingId> result =
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.
Tick tick = 0;
while (f.bs.allBuildings().empty() && tick < 100000)
while (f.bs.getAllBuildings().empty() && tick < 100000)
{
runTicks(f.bs, f.belts, 1, tick);
}
REQUIRE(f.bs.allBuildings().size() == 1);
REQUIRE(f.bs.allBuildings()[0].type == BuildingType::Splitter);
REQUIRE(f.bs.getAllBuildings().size() == 1);
REQUIRE(f.bs.getAllBuildings()[0].type == BuildingType::Splitter);
// The built splitter is registered with BeltSystem carrying the filters.
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.
entt::entity stationEntity = entt::null;
QVector2D stationCenter;
sim.admin().forEach<StationBodyComponent, FactionComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{
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);
}
});
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);
const entt::entity enemyShip = sim.ships().spawn(
const entt::entity enemyShip = sim.getShips().spawn(
combatDef->id,
QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
/*isEnemy=*/true);
@@ -221,7 +221,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
entt::entity stationEntity = entt::null;
QVector2D stationCenter;
sim.admin().forEach<StationBodyComponent, FactionComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{
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);
}
});
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);
sim.ships().spawn(
sim.getShips().spawn(
combatDef->id,
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
/*isEnemy=*/false);
@@ -259,7 +259,7 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
entt::entity stationEntity = entt::null;
QVector2D stationCenter;
sim.admin().forEach<StationBodyComponent, FactionComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{
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);
}
});
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);
const entt::entity playerShip = sim.ships().spawn(
const entt::entity playerShip = sim.getShips().spawn(
combatDef->id,
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
/*isEnemy=*/false);
@@ -390,17 +390,17 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* combatDef = findCombatShip(sim.config());
const ShipDef* combatDef = findCombatShip(sim.getConfig());
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));
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
sim.getAdmin().get<HealthComponent>(ship).hp = -1.0f;
sim.tick();
REQUIRE_FALSE(sim.admin().isValid(ship));
REQUIRE_FALSE(sim.getAdmin().isValid(ship));
}
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
// threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops
// 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));
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
sim.getAdmin().get<HealthComponent>(ship).hp = -1.0f;
sim.tick();
const std::vector<ScrapInfo> scraps = sim.scraps().allScrapInfo();
const std::vector<ScrapInfo> scraps = sim.getScraps().getAllScrapInfo();
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]")
@@ -427,7 +427,7 @@ TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
Simulation sim(loadConfig(), 42);
// 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)
{
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 ==
"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.
// threat_rate_formula = "x": evaluates to the input value.
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(std::string("ore"));
REQUIRE(a.value() == b.value());
REQUIRE(a.getValue() == b.getValue());
}
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);
b.append(43);
REQUIRE(a.value() != b.value());
REQUIRE(a.getValue() != b.getValue());
}
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("bc"));
REQUIRE(a.value() != b.value());
REQUIRE(a.getValue() != b.getValue());
}
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);
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 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 Formula f = Formula::compile(source);
REQUIRE(f.source() == source);
REQUIRE(f.getSource() == source);
}
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.
static void killEnemyStations(Simulation& sim)
{
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health)
{
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();
for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); }
replayPath = recorderPtr->currentFilePath();
replayPath = recorderPtr->getCurrentFilePath();
recordedFinalChecksum = rec.computeStateChecksum();
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())
{
play.tick();
player.advanceTo(play.currentTick());
player.advanceTo(play.getCurrentTick());
}
REQUIRE_FALSE(player.getDesyncTick().has_value());
REQUIRE(play.currentTick() == 150);
REQUIRE(play.getCurrentTick() == 150);
REQUIRE(play.computeStateChecksum() == recordedFinalChecksum);
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();
manager.setRecorder(std::move(recorder));
for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); }
replayPath = recorderPtr->currentFilePath();
replayPath = recorderPtr->getCurrentFilePath();
manager.setRecorder(nullptr);
}
@@ -234,7 +234,7 @@ TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay
while (!player.isFinished())
{
play.tick();
player.advanceTo(play.currentTick());
player.advanceTo(play.getCurrentTick());
}
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();
for (int i = 0; i < 900; ++i) { rec.tick(); manager.recordTickCheckpoint(); }
replayPath = recorderPtr->currentFilePath();
replayPath = recorderPtr->getCurrentFilePath();
recordedFinalChecksum = rec.computeStateChecksum();
manager.setRecorder(nullptr);
}
@@ -281,11 +281,11 @@ TEST_CASE("a long recorded run (through waves and combat) replays with no desync
while (!player.isFinished())
{
play.tick();
player.advanceTo(play.currentTick());
player.advanceTo(play.getCurrentTick());
}
REQUIRE_FALSE(player.getDesyncTick().has_value());
REQUIRE(play.currentTick() == 2400);
REQUIRE(play.getCurrentTick() == 2400);
REQUIRE(play.computeStateChecksum() == recordedFinalChecksum);
QFile::remove(QString::fromStdString(replayPath));

View File

@@ -120,7 +120,7 @@ TEST_CASE("ReplayRecorder writes a well-formed file", "[replay]")
recorder.recordChecksum(30, 0x0ffffffffffffff0ull);
const std::string path = recorder.currentFilePath();
const std::string path = recorder.getCurrentFilePath();
REQUIRE_FALSE(path.empty());
recorder.close();
@@ -152,7 +152,7 @@ TEST_CASE("CommandManager records commands and an initial checksum on drain", "[
ReplayRecorder* recorderPtr = recorder.get();
// setRecorder opens the file and writes the header + the tick-0 checksum.
manager.setRecorder(std::move(recorder));
const std::string path = recorderPtr->currentFilePath();
const std::string path = recorderPtr->getCurrentFilePath();
REQUIRE_FALSE(path.empty());
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());
recorder.startNewRun(1u, 0ull);
const std::string first = recorder.currentFilePath();
const std::string first = recorder.getCurrentFilePath();
recorder.startNewRun(2u, 0ull);
const std::string second = recorder.currentFilePath();
const std::string second = recorder.getCurrentFilePath();
REQUIRE(first != second);
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());
ReplayRecorder* recorderPtr = recorder.get();
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>();
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.drain();
const std::string secondPath = recorderPtr->currentFilePath();
const std::string secondPath = recorderPtr->getCurrentFilePath();
REQUIRE(firstPath != secondPath);
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(4.0f, 5.0f), 6, 200);
const std::vector<ScrapInfo> info = ss.allScrapInfo();
const std::vector<ScrapInfo> info = ss.getAllScrapInfo();
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 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);
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 ModuleDef& modDef : sim.config().modules.modules)
for (const ModuleDef& modDef : sim.getConfig().modules.modules)
{
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]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr);
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);
REQUIRE(sim.admin().isValid(e));
CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(expectedHp));
REQUIRE(sim.getAdmin().isValid(e));
CHECK(sim.getAdmin().get<HealthComponent>(e).maxHp == Approx(expectedHp));
}
TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr);
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;
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);
REQUIRE(sim.admin().isValid(e));
REQUIRE(sim.getAdmin().isValid(e));
// armor_plate has multiplied_hp_formula = "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.admin().get<HealthComponent>(e).hp == sim.admin().get<HealthComponent>(e).maxHp);
CHECK(sim.getAdmin().get<HealthComponent>(e).maxHp == Approx(baseHp * 1.5f));
CHECK(sim.getAdmin().get<HealthComponent>(e).hp == sim.getAdmin().get<HealthComponent>(e).maxHp);
}
TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
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;
ShipLayoutConfig layout;
@@ -162,19 +162,19 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
pm.rotation = Rotation::East;
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);
REQUIRE(sim.admin().isValid(e));
REQUIRE(sim.getAdmin().isValid(e));
// sensor_booster has added_sensor_range_m_formula = "100" m → 100/10 = 10 tiles
// 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]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr);
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);
}
const entt::entity e = sim.ships().spawn("interceptor",
const entt::entity e = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout);
REQUIRE(sim.admin().isValid(e));
REQUIRE(sim.getAdmin().isValid(e));
// Two armor_plates: each 1.5 multiplier
// total_mult = 1 + (1.5 - 1) + (1.5 - 1) = 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]")
{
Simulation sim(loadConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
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);
const Building* b = sim.buildings().findBuilding(yardId);
const Building* b = sim.getBuildings().findBuilding(yardId);
REQUIRE(b != nullptr);
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10
@@ -234,9 +234,9 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
"[modules][shipyard]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -247,7 +247,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
fillMaterials(sim, yardId, *def, emptyLayout);
sim.tick();
const Building* b1 = sim.buildings().findBuilding(yardId);
const Building* b1 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->production.has_value());
@@ -261,7 +261,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b2 = sim.buildings().findBuilding(yardId);
const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->production.has_value());
}
@@ -270,7 +270,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
"[modules][shipyard]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr);
// 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
@@ -278,7 +278,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
// base-hull materials it was charged.
REQUIRE_FALSE(def->defaultModules.empty());
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
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.
entt::entity built = entt::null;
sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity e, const ShipIdentityComponent& si, const FactionComponent& fac)
{
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.
const bool hasWeapon =
findFirstWeaponChild(sim.admin(), built) != entt::null;
findFirstWeaponChild(sim.getAdmin(), built) != entt::null;
CHECK_FALSE(hasWeapon);
}
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
{
Simulation sim(loadConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
@@ -327,13 +327,13 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
layout.placedModules.push_back(pm);
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->shipLayout.has_value());
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
const Building* b2 = sim.buildings().findBuilding(yardId);
const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->shipLayout.has_value());
}
@@ -342,7 +342,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
"[modules][shipyard]")
{
Simulation sim(loadConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
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);
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->shipLayout.has_value());
// Re-selecting the same recipe must be a no-op and preserve the layout.
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->shipLayout.has_value());
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]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr);
ShipLayoutConfig layout;
@@ -390,22 +390,22 @@ TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[mo
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);
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
REQUIRE(sim.admin().isValid(weapon));
const entt::entity weapon = findFirstWeaponChild(sim.getAdmin(), ship);
REQUIRE(sim.getAdmin().isValid(weapon));
// 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]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
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;
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);
}
const entt::entity ship = sim.ships().spawn("interceptor",
const entt::entity ship = sim.getShips().spawn("interceptor",
QVector2D(5.0f, 5.0f), false, layout);
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
REQUIRE(sim.admin().isValid(weapon));
const entt::entity weapon = findFirstWeaponChild(sim.getAdmin(), ship);
REQUIRE(sim.getAdmin().isValid(weapon));
// 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]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
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 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;
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);
// added_main_acceleration_mpss = 60; same conversion as base: / 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]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findSchematic(sim.config(), "interceptor");
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
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 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;
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);
// added_maneuvering_acceleration_mpss = 10; same conversion as base: / 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)
{
int n = 0;
sim.admin().forEach<ShipIdentityComponent>(
sim.getAdmin().forEach<ShipIdentityComponent>(
[&n](entt::entity /*e*/, const ShipIdentityComponent& /*si*/) { ++n; });
return n;
}
@@ -85,9 +85,9 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.config());
const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
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);
bool foundPlayerShip = false;
sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f)
{
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);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
const int shipsBefore = countShips(sim);
@@ -146,9 +146,9 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.config());
const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
const int shipsBefore = countShips(sim);
@@ -170,9 +170,9 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
{
Simulation sim(loadConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.config());
const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.config());
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr);
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
// and is not still running).
bool productionCleared = false;
for (const Building& b : sim.buildings().allBuildings())
for (const Building& b : sim.getBuildings().getAllBuildings())
{
if (b.id == yardId)
{

View File

@@ -19,7 +19,7 @@ TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
{
const Simulation sim(loadConfig());
REQUIRE(sim.currentTick() == 0);
REQUIRE(sim.getCurrentTick() == 0);
}
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
@@ -28,7 +28,7 @@ TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
sim.tick();
REQUIRE(sim.currentTick() == 1);
REQUIRE(sim.getCurrentTick() == 1);
}
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();
}
REQUIRE(sim.currentTick() == 10);
REQUIRE(sim.getCurrentTick() == 10);
}
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.
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).
struct SimulationTestAccess
{
static BuildingSystem& buildings(Simulation& sim) { return sim.buildingsMutable(); }
static BeltSystem& belts(Simulation& sim) { return sim.beltsMutable(); }
static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); }
static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); }
static BuildingId place(Simulation& sim, BuildingType type, QPoint anchor,
Rotation rotation)

View File

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

View File

@@ -49,7 +49,7 @@ TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
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",
@@ -60,7 +60,7 @@ TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside q
WaveSystem ws(cfg, rng);
// 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]")
@@ -71,14 +71,14 @@ TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wav
std::mt19937 rng(42);
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)
{
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]")
@@ -87,11 +87,11 @@ TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruct
std::mt19937 rng(42);
WaveSystem ws(cfg, rng);
REQUIRE(ws.generation() == 0);
REQUIRE(ws.getGeneration() == 0);
ws.onEnemyStationsDestroyed();
REQUIRE(ws.generation() == 1);
REQUIRE(ws.getGeneration() == 1);
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).
int hqCount = 0;
for (const Building& b : sim.buildings().allBuildings())
for (const Building& b : sim.getBuildings().getAllBuildings())
{
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.
int playerCount = 0;
int enemyCount = 0;
sim.admin().forEach<StationBodyComponent, FactionComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
{
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 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;
float actualHp = 0.0f;
sim.admin().forEach<HqProxyComponent, HealthComponent>(
sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
[&](entt::entity /*e*/, const HqProxyComponent& /*hq*/, const HealthComponent& h)
{
found = true;
@@ -147,7 +147,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
{
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; }
// 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);
int armedPlayerStations = 0;
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>(
sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
{
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; }
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner);
if (!sim.getAdmin().hasAll<StationBodyComponent>(mo.owner)) { return; }
const FactionComponent& f = sim.getAdmin().get<FactionComponent>(mo.owner);
if (!f.isEnemy)
{
++armedPlayerStations;
@@ -186,11 +186,11 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
Simulation sim(loadConfig(), 42);
int armedEnemyStations = 0;
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>(
sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
{
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; }
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner);
if (!sim.getAdmin().hasAll<StationBodyComponent>(mo.owner)) { return; }
const FactionComponent& f = sim.getAdmin().get<FactionComponent>(mo.owner);
if (f.isEnemy)
{
++armedEnemyStations;
@@ -221,7 +221,7 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
sim.tick();
if (!foundEnemyShip)
{
sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const FactionComponent& f)
{
@@ -253,7 +253,7 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
Simulation sim(loadConfig(), 42);
// 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)
{
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.
int enemyCount = 0;
sim.admin().forEach<StationBodyComponent, FactionComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
{
if (f.isEnemy) { ++enemyCount; }
@@ -275,7 +275,7 @@ TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
{
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)
{
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);
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{
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)
{
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 (!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 (!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; }
}
@@ -332,7 +332,7 @@ TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
{
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)
{
if (f.isEnemy) { h.hp = -1.0f; }
@@ -352,7 +352,7 @@ TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]
{
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)
{
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.
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)
{
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)
{
if (f.isEnemy) { h.hp = -1.0f; }
@@ -387,7 +387,7 @@ TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
sim.tick();
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)
{
if (f.isEnemy && sb.anchor.x() > newX)

View File

@@ -12,12 +12,13 @@
#include "DisplayName.h"
#include "EventManager.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)
, m_sim(sim)
, m_config(config)
, m_activeIndex(-1)
{
QGridLayout* layout = new QGridLayout(this);
layout->setSpacing(4);
@@ -79,24 +80,42 @@ BuildButtonGrid::~BuildButtonGrid()
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)
{
const BuildingType type = m_types[i];
const std::map<BuildingType, int>::const_iterator it = m_costs.find(type);
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()
{
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)
@@ -105,8 +124,9 @@ void BuildButtonGrid::onBuildButton(int index)
{
return;
}
const std::size_t idx = static_cast<std::size_t>(index);
if (m_activeIndex == index)
if (m_activeIndex == idx)
{
clearActiveButton();
EventManager::getInstance()->sendEventImmediately(
@@ -114,15 +134,15 @@ void BuildButtonGrid::onBuildButton(int index)
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_buttons[static_cast<std::size_t>(index)]->setChecked(true);
m_activeIndex = idx;
m_buttons[idx]->setChecked(true);
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*/)
@@ -130,6 +150,11 @@ void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent>
clearActiveButton();
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
{
updateAffordability();
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event)
{
m_demolishButton->setChecked(event->active);

View File

@@ -1,46 +1,56 @@
#pragma once
#include <map>
#include <optional>
#include <vector>
#include <QWidget>
#include "BuilderModeExitedEvent.h"
#include "BuildHotkeyPressedEvent.h"
#include "BuildingBlocksChangedEvent.h"
#include "BuildingType.h"
#include "DemolishModeChangedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h"
class QPushButton;
class Simulation;
class BuildButtonGrid : public QWidget,
public CombinedEventHandler<BuilderModeExitedEvent,
DemolishModeChangedEvent,
BuildHotkeyPressedEvent>
BuildHotkeyPressedEvent,
BuildingBlocksChangedEvent>
{
Q_OBJECT
public:
BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr);
BuildButtonGrid(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
~BuildButtonGrid() override;
void updateAffordability(int buildingBlocks);
void clearActiveButton();
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 DemolishModeChangedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
private slots:
void onBuildButton(int index);
private:
Simulation* m_sim;
const GameConfig* m_config;
std::vector<BuildingType> m_types;
std::vector<QPushButton*> m_buttons;
std::map<BuildingType, int> m_costs;
int m_activeIndex;
std::optional<std::size_t> m_activeIndex;
QPushButton* m_demolishButton;
};

View File

@@ -19,6 +19,7 @@
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QPolygonF>
#include <QRadialGradient>
@@ -211,7 +212,7 @@ void GameWorldView::onFrame()
if (m_replayPlayer->isFinished()
|| m_sim->isWon() || m_sim->isGameOver()) { break; }
m_sim->tick();
m_replayPlayer->advanceTo(m_sim->currentTick());
m_replayPlayer->advanceTo(m_sim->getCurrentTick());
}
}
else
@@ -227,6 +228,12 @@ void GameWorldView::onFrame()
{
m_viewResetPending = false;
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
@@ -263,7 +270,7 @@ void GameWorldView::onFrame()
// Expire old beams. Lifetime is measured in game ticks so beams stay
// 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;
for (const ActiveBeam& b : m_activeBeams)
{
@@ -314,11 +321,11 @@ void GameWorldView::onFrame()
// Fire events for any state that changed since the last frame
{
const Tick newTick = m_sim->currentTick();
const int newBlocks = m_sim->buildingBlocksStock();
const int newExpCost = m_sim->currentExpansionCost();
const int newBoss = m_sim->bossWaveCounter();
const Tick newCountdown = m_sim->bossCountdownTicks();
const Tick newTick = m_sim->getCurrentTick();
const int newBlocks = m_sim->getBuildingBlocksStock();
const int newExpCost = m_sim->getCurrentExpansionCost();
const int newBoss = m_sim->getBossWaveCounter();
const Tick newCountdown = m_sim->getBossCountdownTicks();
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
// 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
// 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)
{
m_lastArtifactCount = currentArtifactCount;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<ArtifactCountChangedEvent>(
currentArtifactCount, m_sim->config().world.artifacts.artifactWinCount));
currentArtifactCount, m_sim->getConfig().world.artifacts.artifactWinCount));
}
update();
@@ -425,6 +432,8 @@ void GameWorldView::paintGL()
drawBeams(painter);
drawOverlays(painter);
drawScreenSpace(painter);
drawPauseBorder(painter);
drawDemolishBorder(painter);
drawReplayOverlay(painter);
}
@@ -432,27 +441,27 @@ void GameWorldView::paintGL()
// Coordinate helpers
// ---------------------------------------------------------------------------
float GameWorldView::tilePx() const
float GameWorldView::getTilePx() const
{
if (m_config->world.heightTiles <= 0) { return 1.0f; }
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
{
return QPointF(
static_cast<qreal>((worldPos.x() - viewLeftTiles()) * tilePx()),
static_cast<qreal>(worldPos.y() * tilePx()));
static_cast<qreal>((worldPos.x() - getViewLeftTiles()) * getTilePx()),
static_cast<qreal>(worldPos.y() * getTilePx()));
}
QPointF GameWorldView::tileToWidget(QPoint tile) const
@@ -463,15 +472,15 @@ QPointF GameWorldView::tileToWidget(QPoint tile) const
QPoint GameWorldView::widgetToTile(QPoint widgetPt) const
{
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + viewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / tilePx();
const float wx = static_cast<float>(widgetPt.x()) / getTilePx() + getViewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / getTilePx();
return QPoint(static_cast<int>(std::floor(wx)), static_cast<int>(std::floor(wy)));
}
QVector2D GameWorldView::widgetToWorld(QPoint widgetPt) const
{
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + viewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / tilePx();
const float wx = static_cast<float>(widgetPt.x()) / getTilePx() + getViewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / getTilePx();
return QVector2D(wx, wy);
}
@@ -479,22 +488,22 @@ QRectF GameWorldView::tileRect(QPoint tile) const
{
const QPointF tl = tileToWidget(tile);
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 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;
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());
for (const Building& b : m_sim->buildings().allBuildings())
float leftX = -static_cast<float>(m_sim->getCurrentAsteroidWidth_tiles());
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
@@ -507,11 +516,11 @@ float GameWorldView::asteroidLeftEdge() const
return leftX;
}
float GameWorldView::enemyStationRightEdge() const
float GameWorldView::getEnemyStationRightEdge() const
{
float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth_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)
{
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 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 rightEdge = enemyStationRightEdge();
const float rightEdge = getEnemyStationRightEdge();
// 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
@@ -562,8 +571,8 @@ void GameWorldView::clampScroll()
// 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
// until the enemy stations are centered (revealing a little space beyond them).
const float leftBound = asteroidLeftEdge();
const float rightBound = enemyStationRightEdge();
const float leftBound = getAsteroidLeftEdge();
const float rightBound = getEnemyStationRightEdge();
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
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
// rotate-in-place check.
if (!m_sim->buildings().isPlacementValid(type, anchor, rot))
if (!m_sim->getBuildings().isPlacementValid(type, anchor, rot))
{
return false;
}
@@ -598,7 +607,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
bool anyOccupied = false;
for (const QPoint& relCell : parsed.bodyCells)
{
if (m_sim->buildings().isTileOccupied(anchor + relCell))
if (m_sim->getBuildings().isTileOccupied(anchor + relCell))
{
anyOccupied = true;
break;
@@ -607,14 +616,14 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
if (anyOccupied)
{
return m_sim->buildings().findRotateInPlaceTarget(type, anchor, rot).has_value();
return m_sim->getBuildings().findRotateInPlaceTarget(type, anchor, rot).has_value();
}
return true;
}
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)
{
@@ -629,7 +638,7 @@ BuildingId GameWorldView::buildingAtTile(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)
{
@@ -651,7 +660,7 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
const int y1 = std::max(cornerA.y(), cornerB.y());
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)
{
@@ -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)
{
@@ -680,11 +689,11 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
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 m_sim->admin().get<PositionComponent>(entity).value;
return m_sim->getAdmin().get<PositionComponent>(entity).value;
}
void GameWorldView::clearScrapSelection()
@@ -700,7 +709,7 @@ void GameWorldView::pruneDespawnedScrap()
if (m_selectedScrap.empty()) { return; }
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)
!= m_selectedScrap.end())
@@ -747,7 +756,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
int totalCost = 0;
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())
{
continue;
@@ -755,13 +764,13 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
const BuildingDef* def = findBuildingDef(bb.type);
if (def) { totalCost += def->cost; }
}
if (m_sim->buildingBlocksStock() < totalCost) { return; }
if (m_sim->getBuildingBlocksStock() < totalCost) { return; }
for (const BlueprintBuilding& bb : bp.buildings)
{
const QPoint anchor = center + bb.offset;
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())
{
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
@@ -832,7 +841,7 @@ void GameWorldView::placeAtTile(QPoint tile)
}
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())
{
std::shared_ptr<RotateInPlaceCommand> command =
@@ -853,7 +862,7 @@ void GameWorldView::placeAtTile(QPoint tile)
{
return;
}
if (!m_sim->buildings().isTileOccupied(tile) && canAfford(type))
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
{
enqueuePlaceBuilding(type, tile, m_ghostRotation);
m_beltDragTiles.insert(tile);
@@ -863,7 +872,7 @@ void GameWorldView::placeAtTile(QPoint tile)
|| type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit)
{
if (!m_sim->buildings().isTileOccupied(tile) && canAfford(type))
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
{
enqueuePlaceBuilding(type, tile, m_ghostRotation);
if (type == BuildingType::TunnelEntry)
@@ -886,31 +895,39 @@ void GameWorldView::placeAtTile(QPoint tile)
// Port glyph helper
// ---------------------------------------------------------------------------
void GameWorldView::drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color)
void GameWorldView::drawPortGlyph(QPainter& painter, QPoint tile,
Rotation direction, const QColor& color,
bool centered)
{
const float px = tilePx();
const QRectF tr = tileRect(bodyTile);
const float px = getTilePx();
const QRectF tr = tileRect(tile);
const QPointF center(tr.x() + static_cast<qreal>(px) * 0.5,
tr.y() + static_cast<qreal>(px) * 0.5);
QPointF offset;
QPointF edgeOffset;
const char* ch;
switch (direction)
{
case Rotation::East: offset = QPointF(px * 0.25f, 0); ch = ">"; break;
case Rotation::West: offset = QPointF(-px * 0.25f, 0); ch = "<"; break;
case Rotation::North: offset = QPointF(0, -px * 0.25f); ch = "^"; break;
case Rotation::South: offset = QPointF(0, px * 0.25f); ch = "v"; break;
case Rotation::East: edgeOffset = QPointF(px * 0.25f, 0); ch = ">"; break;
case Rotation::West: edgeOffset = QPointF(-px * 0.25f, 0); ch = "<"; break;
case Rotation::North: edgeOffset = QPointF(0, -px * 0.25f); ch = "^"; break;
case Rotation::South: edgeOffset = QPointF(0, px * 0.25f); ch = "v"; break;
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 QRectF textRect(pos.x() - half, pos.y() - half, half * 2.0, half * 2.0);
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.setPen(color);
painter.drawText(textRect, Qt::AlignCenter, QString::fromLatin1(ch));
@@ -922,13 +939,13 @@ void GameWorldView::drawPortGlyph(QPainter& painter, QPoint bodyTile,
void GameWorldView::drawTiles(QPainter& painter)
{
const int leftTile = static_cast<int>(std::floor(viewLeftTiles())) - 1;
const int rightTile = leftTile + static_cast<int>(std::ceil(viewportWidthTiles())) + 2;
const int leftTile = static_cast<int>(std::floor(getViewLeftTiles())) - 1;
const int rightTile = leftTile + static_cast<int>(std::ceil(getViewportWidthTiles())) + 2;
const int bottomTile = m_config->world.heightTiles;
// Asteroid columns left of the buildable edge are not yet unlocked by
// 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);
for (int x = leftTile; x <= rightTile; ++x)
@@ -951,7 +968,7 @@ void GameWorldView::drawTiles(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 =
m_visuals->buildings.find(b.type);
@@ -966,8 +983,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
const QPointF tl = tileToWidget(b.anchor);
const QRectF bboxRect(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(tilePx()),
b.footprint.height() * static_cast<qreal>(tilePx()));
b.footprint.width() * static_cast<qreal>(getTilePx()),
b.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush);
@@ -982,13 +999,13 @@ void GameWorldView::drawBuildings(QPainter& painter)
for (const Port& port : b.outputPorts)
{
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.
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*/,
const FactionComponent& f, const HealthComponent& h)
{
@@ -1002,7 +1019,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
}
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 =
m_visuals->buildings.find(s.type);
@@ -1016,8 +1033,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
const QPointF tl = tileToWidget(s.anchor);
const QRectF bboxRect(tl.x(), tl.y(),
s.footprint.width() * static_cast<qreal>(tilePx()),
s.footprint.height() * static_cast<qreal>(tilePx()));
s.footprint.width() * static_cast<qreal>(getTilePx()),
s.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1, Qt::DashLine));
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect);
@@ -1030,7 +1047,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
int pct = 0;
if (s.completesAt > 0 && durationTicks > 0)
{
const Tick elapsed = m_sim->currentTick()
const Tick elapsed = m_sim->getCurrentTick()
- (s.completesAt - durationTicks);
pct = static_cast<int>(
std::max(Tick(0), std::min(durationTicks, elapsed))
@@ -1061,7 +1078,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
{
const QPoint absBody = s.anchor
+ 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<QSize> footprint;
if (const Building* b = m_sim->buildings().findBuilding(id))
if (const Building* b = m_sim->getBuildings().findBuilding(id))
{
anchor = b->anchor;
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;
footprint = s->footprint;
@@ -1093,8 +1111,8 @@ std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const
const QPointF tl = tileToWidget(*anchor);
return QRectF(tl.x(), tl.y(),
footprint->width() * static_cast<qreal>(tilePx()),
footprint->height() * static_cast<qreal>(tilePx()));
footprint->width() * static_cast<qreal>(getTilePx()),
footprint->height() * static_cast<qreal>(getTilePx()));
}
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
// 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())
{
const qreal outlineRadius = static_cast<qreal>(tilePx() * 0.2f) + 3.0;
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo())
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo())
{
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), scrap.entity)
== m_selectedScrap.end()) { continue; }
@@ -1136,13 +1154,13 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
painter.setPen(Qt::NoPen);
painter.setBrush(color);
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; }
const std::optional<QRectF> rect = footprintWidgetRect(b.id);
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; }
const std::optional<QRectF> rect = footprintWidgetRect(s.id);
@@ -1163,7 +1181,7 @@ void GameWorldView::drawCopyConfigFeedback(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
// 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
// items on belts stay fully visible.
constexpr double kPortMarginTiles = 0.2;
const double margin = kPortMarginTiles * static_cast<double>(tilePx());
const double margin = kPortMarginTiles * static_cast<double>(getTilePx());
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
|| b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
@@ -1218,17 +1236,17 @@ void GameWorldView::drawPortItems(QPainter& painter)
painter.save();
painter.setClipRegion(clip);
m_sim->buildings().forEachEmergingItem(drawItem);
m_sim->buildings().forEachIncomingItem(drawItem);
m_sim->getBuildings().forEachEmergingItem(drawItem);
m_sim->getBuildings().forEachIncomingItem(drawItem);
painter.restore();
}
void GameWorldView::drawBeltItems(QPainter& painter)
{
const float halfPx = tilePx() * 0.5f * 0.5f;
const QRect vr = viewportRect();
const float halfPx = getTilePx() * 0.5f * 0.5f;
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 =
m_visuals->items.find(vi.type.id);
@@ -1248,8 +1266,8 @@ void GameWorldView::drawBeltItems(QPainter& painter)
void GameWorldView::drawScrap(QPainter& painter)
{
const float r = tilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo())
const float r = getTilePx() * 0.2f;
for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo())
{
const QPointF center = worldToWidget(scrap.position);
painter.setBrush(QColor(128, 110, 90));
@@ -1261,7 +1279,7 @@ void GameWorldView::drawScrap(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,
const HealthComponent& h)
{
@@ -1281,8 +1299,8 @@ void GameWorldView::drawStations(QPainter& painter)
const QPointF tl = tileToWidget(QPoint(sb.anchor.x(), sb.anchor.y()));
const QRectF bboxRect(tl.x(), tl.y(),
sb.footprint.width() * static_cast<qreal>(tilePx()),
sb.footprint.height() * static_cast<qreal>(tilePx()));
sb.footprint.width() * static_cast<qreal>(getTilePx()),
sb.footprint.height() * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush);
@@ -1306,7 +1324,7 @@ void GameWorldView::drawStations(QPainter& painter)
void GameWorldView::drawShips(QPainter& painter)
{
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>(
[&](entt::entity e, const ShipIdentityComponent& si,
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 perp(-dir.y(), dir.x());
const float fwd = tilePx() * 0.45f;
const float side = tilePx() * 0.25f;
const float fwd = getTilePx() * 0.45f;
const float side = getTilePx() * 0.25f;
QPolygonF tri;
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,
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);
painter.fillRect(QRectF(left, top, width, barH), QColor(60, 60, 60));
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)
{
painter.setBrush(Qt::NoBrush);
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& /*facing*/,
@@ -1378,7 +1396,7 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
const QPointF center = worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(tilePx());
* static_cast<qreal>(getTilePx());
QColor circleColor = it->second.outline;
circleColor.setAlpha(77);
painter.setPen(QPen(circleColor, 1));
@@ -1404,7 +1422,7 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
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,
const PositionComponent& pos, const AttackBehavior& attack)
{
@@ -1417,7 +1435,7 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
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,
const PositionComponent& pos, const RepairBehavior& repair)
{
@@ -1430,7 +1448,7 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
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,
const PositionComponent& pos, const SalvageScrapBehavior& salvage)
{
@@ -1446,15 +1464,15 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
const QStringList lines = {
tr("Accumulated Threat Level: %1")
.arg(m_sim->threatLevel(), 0, 'f', 1),
.arg(m_sim->getThreatLevel(), 0, 'f', 1),
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")
.arg(m_sim->threatAccumulationRate(), 0, 'f', 1),
.arg(m_sim->getThreatAccumulationRate(), 0, 'f', 1),
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")
.arg(m_sim->currentFactoryProductionThreatRate(), 0, 'f', 1),
.arg(m_sim->getCurrentFactoryProductionThreatRate(), 0, 'f', 1),
};
QFont font = painter.font();
@@ -1562,7 +1580,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
if (m_builderType.has_value())
{
drawBuildingGhost(painter, *m_builderType, m_ghostTile,
m_ghostRotation, m_ghostValid);
m_ghostRotation, m_ghostValid,
/*showPortTargetGlyphs*/ true);
}
// Blueprint placement ghost
@@ -1572,7 +1591,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
{
const QPoint anchor = m_blueprintGhostTile + bb.offset;
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))
{
const Building* b = m_sim->buildings().findBuilding(id);
const Building* b = m_sim->getBuildings().findBuilding(id);
if (b && b->type == BuildingType::Hq) { continue; }
const std::vector<QPoint>* cells = nullptr;
const ConstructionSite* s = nullptr;
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)
{
for (const QPoint& cell : *cells)
@@ -1599,7 +1619,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
}
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)
{
for (const QPoint& cell : b->bodyCells)
@@ -1625,7 +1645,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation,
bool valid)
bool valid, bool showPortTargetGlyphs)
{
const BuildingDef* def = findBuildingDef(type);
if (!def) { return; }
@@ -1663,8 +1683,8 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
const QPointF tl = tileToWidget(anchorTile + minCell);
const QRectF bboxRect(tl.x(), tl.y(),
(maxCell.x() - minCell.x() + 1) * static_cast<qreal>(tilePx()),
(maxCell.y() - minCell.y() + 1) * static_cast<qreal>(tilePx()));
(maxCell.x() - minCell.x() + 1) * static_cast<qreal>(getTilePx()),
(maxCell.y() - minCell.y() + 1) * static_cast<qreal>(getTilePx()));
painter.setPen(QPen(lineColor, 1));
painter.setBrush(Qt::NoBrush);
@@ -1679,7 +1699,24 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
for (const Port& port : parsed.outputPorts)
{
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);
@@ -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)
{
if (!m_replayPlayer) { return; }
@@ -1932,7 +2042,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
}
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)
{
@@ -1983,7 +2093,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
}
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
// 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).
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; }
std::shared_ptr<DemolishCommand> command =
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
// (REQ-UI-SCRAP-MULTI-SELECT).
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 (!m_selectedBuildingIds.empty())
@@ -2260,7 +2370,7 @@ void GameWorldView::pasteConfigTo(BuildingId id)
{
// Operational splitters are configured by tile; sites by BuildingId
// (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::make_shared<SetSplitterFiltersCommand>();
@@ -2338,7 +2448,7 @@ void GameWorldView::exitBuilderMode()
std::make_shared<BuilderModeExitedEvent>());
}
double GameWorldView::gameSpeed() const
double GameWorldView::getGameSpeed() const
{
return m_gameSpeedMultiplier;
}
@@ -2391,6 +2501,12 @@ void GameWorldView::resetForNewGame()
EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
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();
}
@@ -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):
// 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;
if (m_sim->admin().isValid(event->target)
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
if (m_sim->getAdmin().isValid(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());
maxRadius = shorter / 2.0f;
}
else if (m_sim->admin().isValid(event->target)
&& m_sim->admin().hasAll<ScrapDataComponent>(event->target))
else if (m_sim->getAdmin().isValid(event->target)
&& m_sim->getAdmin().hasAll<ScrapDataComponent>(event->target))
{
maxRadius = 0.1f;
}
@@ -2488,5 +2604,5 @@ bool GameWorldView::canAfford(BuildingType type) const
{
const BuildingDef* def = findBuildingDef(type);
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);
~GameWorldView() override;
double gameSpeed() const;
double getGameSpeed() const;
bool isDebugDrawEnabled() const;
void resetFrameTimer();
void setGameSpeed(double multiplier);
@@ -131,25 +131,37 @@ private:
void drawBeams(QPainter& painter);
void drawOverlays(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);
float tilePx() const;
float viewportWidthTiles() const;
float getTilePx() const;
float getViewportWidthTiles() const;
// 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.
float viewLeftTiles() const;
float getViewLeftTiles() const;
QPointF worldToWidget(QVector2D worldPos) const;
QPointF tileToWidget(QPoint tile) const;
QPoint widgetToTile(QPoint widgetPt) const;
QRectF tileRect(QPoint tile) const;
QRect viewportRect() const;
QRect getViewportRect() const;
// Widget-space rectangle covering a building or construction site's footprint,
// or nullopt if the id resolves to neither. Shared by the selection highlight
// and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK).
std::optional<QRectF> footprintWidgetRect(BuildingId id) const;
float asteroidLeftEdge() const;
float enemyStationRightEdge() const;
float getAsteroidLeftEdge() const;
float getEnemyStationRightEdge() const;
// Horizontal pan speed at a given view-center X, in tiles/s (REQ-UI-SCROLL-SPEED).
float panSpeedTilesPerSecondAt(float viewCenterXTiles) const;
void clampScroll();
@@ -163,11 +175,13 @@ private:
std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const;
QVector2D widgetToWorld(QPoint widgetPt) const;
void drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color);
void drawPortGlyph(QPainter& painter, QPoint tile,
Rotation direction, const QColor& color,
bool centered);
void drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation, bool valid);
QPoint anchorTile, Rotation rotation, bool valid,
bool showPortTargetGlyphs);
void placeBlueprintAtTile(QPoint center);
@@ -222,7 +236,7 @@ private:
std::mt19937 m_rng;
double m_gameSpeedMultiplier;
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;
QTimer* m_renderTimer;

View File

@@ -32,6 +32,11 @@ HeaderBar::HeaderBar(const GameConfig* config, QWidget* parent)
QString::fromStdString(*config->world.buildingBlocksTooltip));
}
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);
layout->addWidget(m_timeLabel);
layout->addWidget(m_blocksLabel);

View File

@@ -14,7 +14,6 @@
#include "BlueprintPanel.h"
#include "BuildButtonGrid.h"
#include "BuildingBlocksChangedEvent.h"
#include "BuildingSystem.h"
#include "Command.h"
#include "CommandRequestedEvent.h"
@@ -42,9 +41,9 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
setWindowTitle(tr("Dota Factory"));
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_sidePanel = new QWidget(this);
@@ -52,9 +51,9 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
sideLayout->setContentsMargins(1, 1, 1, 1);
sideLayout->setSpacing(1);
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_sidePanel);
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_sidePanel);
m_blueprintPanel = new BlueprintPanel(sim, &sim->config(), m_sidePanel);
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), m_sidePanel);
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
sideLayout->addWidget(m_selectedBuildingPanel, 1);
sideLayout->addWidget(m_buildButtonGrid, 1);
@@ -151,18 +150,13 @@ void MainWindow::layoutPanels()
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)
{
const double prevSpeed = m_gameWorldView->gameSpeed();
const double prevSpeed = m_gameWorldView->getGameSpeed();
m_gameWorldView->setGameSpeed(0.0);
ModalDimScope dim(*m_dimOverlay);
SchematicChoiceDialog dialog(event->choices, m_sim->config().recipes, this);
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes, this);
dialog.exec();
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*/)
{
const double prevSpeed = m_gameWorldView->gameSpeed();
const double prevSpeed = m_gameWorldView->getGameSpeed();
m_gameWorldView->setGameSpeed(0.0);
ModalDimScope dim(*m_dimOverlay);
@@ -232,11 +226,11 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
const std::string& schematicId,
const ShipLayoutConfig& currentLayout)
{
const double prevSpeed = m_gameWorldView->gameSpeed();
const double prevSpeed = m_gameWorldView->getGameSpeed();
m_gameWorldView->setGameSpeed(0.0);
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))
{
@@ -245,17 +239,17 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
}
ModalDimScope dim(*m_dimOverlay);
ShipLayoutDialog dialog(&m_sim->config(), schematicId, currentLayout,
ShipLayoutDialog dialog(&m_sim->getConfig(), schematicId, currentLayout,
m_layoutBlueprints,
std::move(unlockedModuleIds),
m_gameWorldView->isDebugDrawEnabled(),
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::make_shared<SetShipLayoutCommand>();
command->id = shipyardId;
command->layout = *dialog.result();
command->layout = *dialog.getResult();
EventManager::getInstance()->sendEventImmediately(
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
// 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 =
b ? nullptr : m_sim->buildings().findSite(event->shipyardId);
b ? nullptr : m_sim->getBuildings().findSite(event->shipyardId);
if (!b && !s)
{
return;
@@ -291,14 +285,14 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
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);
// 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).
const Building* b = m_sim->buildings().findBuilding(event->buildingId);
const Building* b = m_sim->getBuildings().findBuilding(event->buildingId);
const ConstructionSite* s =
b ? nullptr : m_sim->buildings().findSite(event->buildingId);
b ? nullptr : m_sim->getBuildings().findSite(event->buildingId);
if (!b && !s)
{
m_gameWorldView->setGameSpeed(prevSpeed);
@@ -316,7 +310,7 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
// dereferenced after dialog.exec() returns.
const std::string oldSchematic = b ? b->recipeId : s->recipeId;
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)
? tr("Select Schematic")
: tr("Select Recipe");
@@ -356,7 +350,7 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
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 minutes = 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*/)
{
const Tick tick = m_sim->currentTick();
const Tick tick = m_sim->getCurrentTick();
const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
const int minutes = totalSeconds / 60;
const int seconds = totalSeconds % 60;

View File

@@ -6,7 +6,6 @@
#include <QWidget>
#include "BuildingBlocksChangedEvent.h"
#include "BuildingId.h"
#include "EscapeMenuRequestedEvent.h"
#include "EventHandler.h"
@@ -32,8 +31,7 @@ class QCloseEvent;
class QResizeEvent;
class MainWindow : public QWidget,
public CombinedEventHandler<BuildingBlocksChangedEvent,
SchematicChoicesAvailableEvent,
public CombinedEventHandler<SchematicChoicesAvailableEvent,
GameOverEvent,
WinEvent,
EscapeMenuRequestedEvent,
@@ -52,7 +50,6 @@ protected:
void closeEvent(QCloseEvent* event) override;
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 GameOverEvent> 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;
hideAllWidgets();
const Building* b = m_sim->buildings().findBuilding(id);
const ConstructionSite* s = b ? nullptr : m_sim->buildings().findSite(id);
const Building* b = m_sim->getBuildings().findBuilding(id);
const ConstructionSite* s = b ? nullptr : m_sim->getBuildings().findSite(id);
if (!b && !s)
{
buildEmpty();
@@ -353,12 +353,12 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
std::optional<BeltSystem::SplitterInfo> info;
if (m_singleIsSite)
{
info = m_sim->buildings().getSiteSplitterInfo(id);
info = m_sim->getBuildings().getSiteSplitterInfo(id);
}
else
{
m_splitterTile = anchor;
info = m_sim->belts().getSplitterInfo(m_splitterTile);
info = m_sim->getBelts().getSplitterInfo(m_splitterTile);
}
buildSplitterFilters(info);
}
@@ -397,7 +397,7 @@ void SelectedBuildingPanel::refreshSiteProgress(const ConstructionSite* s)
if (def && def->constructionTimeSeconds > 0)
{
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>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = tr("%1% complete").arg(pct);
@@ -554,7 +554,7 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
{
const Tick cycleTicks = secondsToTicks(durationSeconds);
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 int pct = static_cast<int>(
std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks);
@@ -673,7 +673,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
}
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 (m_titleLabel->text().startsWith(tr("(Building) ")))
@@ -686,7 +686,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
}
return;
}
const ConstructionSite* s = m_sim->buildings().findSite(m_singleBuildingId);
const ConstructionSite* s = m_sim->getBuildings().findSite(m_singleBuildingId);
if (s)
{
// 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;
for (BuildingId id : ids)
{
const Building* b = m_sim->buildings().findBuilding(id);
const Building* b = m_sim->getBuildings().findBuilding(id);
if (b)
{
counts[b->type]++;
continue;
}
const ConstructionSite* s = m_sim->buildings().findSite(id);
const ConstructionSite* s = m_sim->getBuildings().findSite(id);
if (s)
{
counts[s->type]++;
@@ -791,7 +791,7 @@ void SelectedBuildingPanel::buildSplitterFilters(
return;
}
const std::vector<std::string> items = allItemIds();
const std::vector<std::string> items = getAllItemIds();
auto populateList = [&](QListWidget* list, QLabel* label,
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;
for (const RecipeDef& recipe : m_config->recipes.recipes)
@@ -888,7 +888,7 @@ void SelectedBuildingPanel::onClearBelt()
std::vector<QPoint> tiles;
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))
{
for (const QPoint& cell : b->bodyCells)
@@ -918,7 +918,7 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEven
m_scrapLabel->hide();
clearContent();
EntityAdmin& admin = m_sim->admin();
EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity))
@@ -944,7 +944,7 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEven
void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
{
EntityAdmin& admin = m_sim->admin();
EntityAdmin& admin = m_sim->getAdmin();
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
const HealthComponent& health = admin.get<HealthComponent>(entity);
@@ -976,7 +976,7 @@ void SelectedBuildingPanel::buildEntityShip(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 bool isEnemy = admin.hasAll<FactionComponent>(entity)
@@ -1019,7 +1019,7 @@ void SelectedBuildingPanel::refreshEntityStats()
{
if (!m_selectedEntity.has_value()) { return; }
EntityAdmin& admin = m_sim->admin();
EntityAdmin& admin = m_sim->getAdmin();
entt::entity entity = *m_selectedEntity;
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).
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)
!= m_selectedScrap.end())

View File

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

View File

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