requirements clarifications

This commit is contained in:
2026-04-18 23:07:22 +02:00
parent 8d4fece87d
commit f29e2ba235
2 changed files with 30 additions and 15 deletions

View File

@@ -23,16 +23,11 @@ This split is enforced at the CMake target level (see below). Tests link only ag
The simulation advances in discrete ticks. All game quantities — production timers, belt item progress, threat accumulation, wave timers, ship cooldowns — are measured in ticks, not wall-clock seconds. The simulation advances in discrete ticks. All game quantities — production timers, belt item progress, threat accumulation, wave timers, ship cooldowns — are measured in ticks, not wall-clock seconds.
- Tick rate: fixed at 30 Hz. - Tick rate: fixed at 30 Hz; `tickDurationMs = 1000 / 30 ≈ 33.33`.
- The outer loop advances `N` ticks per wall-clock frame, where `N` is derived from the selected game speed: - Ticks are driven by an accumulator that is independent of the render rate. Each render frame, the driver adds `elapsedWallMs × gameSpeedMultiplier` to an accumulator and flushes one `tick()` per `tickDurationMs` of accumulated time (so multiple sim ticks may run between frames at high speeds, or a frame may run no ticks at low speeds). `gameSpeedMultiplier` ∈ {0, 0.5, 1, 2, 4} per REQ-UI-SPEED; 0× freezes the accumulator (pause). The concrete driver lives in the Rendering section.
- 0× → 0 ticks/frame (pause)
- 0.5× → one tick every two frames
- 1× → one tick/frame
- 2× → two ticks/frame
- 4× → four ticks/frame
- Config-level durations given in seconds (recipe durations, wave gap ranges, scrap despawn, etc.) are converted to ticks at config-load time. - Config-level durations given in seconds (recipe durations, wave gap ranges, scrap despawn, etc.) are converted to ticks at config-load time.
Consequences: determinism, replayability, and the time-scale feature falls out for free. Consequences: determinism, replayability, and the time-scale feature fall out for free. The simulation advances the same number of ticks over the same amount of game-time regardless of whether the game renders at 60 FPS, 30 FPS, or a stuttery mix.
## Config Loading ## Config Loading
@@ -56,7 +51,7 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
Simulation types shared across subsystems: Simulation types shared across subsystems:
- `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Used for ships and scrap drops. Buildings are addressed by their anchor tile, not by `EntityId`. - `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, scrap drops, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `ThreatResponse.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly.
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed. - `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). - `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation).
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap). - `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
@@ -152,13 +147,17 @@ Buildings are plain structs with a fixed type determined at construction.
```cpp ```cpp
struct Building { struct Building {
EntityId id;
QPoint tile; QPoint tile;
QSize footprint; QSize footprint;
Rotation rotation; Rotation rotation;
BuildingType type; BuildingType type;
float hp; // relevant for HQ and defence stations; ignored otherwise.
float maxHp;
InputBuffer inputBuffer; InputBuffer inputBuffer;
OutputBuffer outputBuffer; OutputBuffer outputBuffer;
// Production timer and current recipe, where applicable. // Production timer, the recipe currently running, and — for reprocessing
// plants — the output item picked at cycle start (REQ-MAT-CYCLE).
std::optional<Production> production; std::optional<Production> production;
}; };
``` ```
@@ -167,6 +166,21 @@ struct Building {
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances. - Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components. - No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
## Scrap
Scrap is the only non-ship, non-building entity in the simulation:
```cpp
struct Scrap {
EntityId id;
QVector2D position; // world units, tile-fractional; ship-center convention
int amount;
Tick despawnAt; // absolute tick at which the scrap is removed
};
```
Created in tick step 9 (Deaths & loot) per REQ-RES-SCRAP-DROP, consumed by salvage ships in tick step 7 (ScrapCollector), and removed in tick step 11 when the current tick reaches `despawnAt`.
## Ships ## Ships
Ships follow a component-composition model using `std::optional<Component>` members. Each orthogonal capability is a component; each behavior is also a component, ticked by its own system. A ship's "role" is just which components it has — not a class or an enum. Ships follow a component-composition model using `std::optional<Component>` members. Each orthogonal capability is a component; each behavior is also a component, ticked by its own system. A ship's "role" is just which components it has — not a class or an enum.

View File

@@ -7,7 +7,7 @@ Config files use the TOML format. The following config files drive game paramete
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, enemy ship level formula, belt speed, starting building blocks. - **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, enemy ship level formula, belt speed, starting building blocks.
- **buildings.toml** — building block cost and construction time per building type. - **buildings.toml** — building block cost and construction time per building type.
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. - **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities.
- **ships.toml** — ship stats (HP, speed, damage, attack range, attack rate, sensor range) as formulas of ship level, required materials per blueprint, threat cost formula, and whether each blueprint is available from the start or unlocked via loot. - **ships.toml** — per blueprint: a human-readable display name (used in toasts and UI), ship stats (HP, speed, damage, attack range, attack rate, sensor range) as formulas of ship level, required build materials, threat cost formula, player production level, and whether the blueprint is available from game start.
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level. - **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors, glyphs, and tile tints for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it. - **visuals.toml** — rendering-only config (not game parameters): fill and outline colors, glyphs, and tile tints for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
@@ -58,6 +58,7 @@ Output port indicators are not building tiles themselves. A building may have mo
- REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed. - REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed.
- REQ-BLD-ROTATE: While in builder mode, pressing E rotates the ghost 90° clockwise and Q rotates it 90° counter-clockwise. Rotation affects the direction of the output port. - REQ-BLD-ROTATE: While in builder mode, pressing E rotates the ghost 90° clockwise and Q rotates it 90° counter-clockwise. Rotation affects the direction of the output port.
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock. - REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock.
- REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — and (b) no footprint cell overlaps an existing placed building or construction site. Affordability is not re-checked at placement time: builder mode cannot be entered when the player cannot afford the building (REQ-UI-BUILD-DISABLED), so once in builder mode the only placement validity concerns are terrain and overlap. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color when the current cursor position fails either condition.
- REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture. - REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture.
- REQ-BLD-DEMOLISH: The player can demolish a placed factory building. Demolition returns `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) to the global stock. The HQ and player defence stations cannot be demolished. - REQ-BLD-DEMOLISH: The player can demolish a placed factory building. Demolition returns `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) to the global stock. The HQ and player defence stations cannot be demolished.
@@ -67,7 +68,7 @@ Output port indicators are not building tiles themselves. A building may have mo
- REQ-BLD-SMELTER: **Smelter** (2×2): Converts ore or scrap into basic materials. No recipe selection required. Inputs, outputs, and rates are defined in `recipes.toml [[recipe]]` entries with `building = "smelter"`. - REQ-BLD-SMELTER: **Smelter** (2×2): Converts ore or scrap into basic materials. No recipe selection required. Inputs, outputs, and rates are defined in `recipes.toml [[recipe]]` entries with `building = "smelter"`.
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. - REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`.
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING. - REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a blueprint. Automatically produces one ship of that type at a fixed level (`ships.toml [ship.blueprint].player_production_level`, default: 5) whenever all required materials (`[ship.blueprint].materials`) are present in its input buffer. - REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a blueprint. Automatically produces one ship of that type at `ships.toml [ship.blueprint].player_production_level` (initial value 5, incremented by duplicate blueprint drops per REQ-DEF-BLUEPRINT-DROP) whenever all required materials (`[ship.blueprint].materials`) are present in its input buffer.
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts. - REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts.
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED). - REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED).
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel. Routing rules: - REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel. Routing rules:
@@ -114,7 +115,7 @@ Output port indicators are not building tiles themselves. A building may have mo
- REQ-SHP-SALVAGE: **Salvage ships** — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it, collect, and deliver it to a Salvage Bay on the asteroid; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Salvage ships are vulnerable to enemy ships while operating. - REQ-SHP-SALVAGE: **Salvage ships** — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it, collect, and deliver it to a Salvage Bay on the asteroid; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Salvage ships are vulnerable to enemy ships while operating.
- REQ-SHP-REPAIR: **Repair ships** — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard: - REQ-SHP-REPAIR: **Repair ships** — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard:
- Defence stations first / ships first / nearest target. - Defence stations first / ships first / nearest target.
- REQ-SHP-BLUEPRINTS: The player selects a blueprint per shipyard by clicking it. New blueprints are unlocked as loot from destroyed enemy defence stations. - REQ-SHP-BLUEPRINTS: The player selects a blueprint per shipyard by clicking it. New blueprints are unlocked automatically when an enemy defence station set is destroyed (REQ-DEF-BLUEPRINT-DROP) — there is no physical loot to collect.
## Defence Stations ## Defence Stations
@@ -133,7 +134,7 @@ Output port indicators are not building tiles themselves. A building may have mo
- REQ-WAV-GAP: At game start and immediately after each wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`]. - REQ-WAV-GAP: At game start and immediately after each wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`].
- REQ-WAV-TRIGGER: When the gap expires, a wave is triggered. Ships are selected one at a time: from all blueprints whose `threat.cost_formula` evaluates to > 0 at the current enemy ship level, uniformly randomly pick one whose cost fits the remaining threat budget. Repeat until no eligible blueprint fits. Any remaining threat carries over to the next wave. A longer gap results in a larger wave. - REQ-WAV-TRIGGER: When the gap expires, a wave is triggered. Ships are selected one at a time: from all blueprints whose `threat.cost_formula` evaluates to > 0 at the current enemy ship level, uniformly randomly pick one whose cost fits the remaining threat budget. Repeat until no eligible blueprint fits. Any remaining threat carries over to the next wave. A longer gap results in a larger wave.
- REQ-WAV-SHIP-LEVEL: Each wave's enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where x is elapsed game time in seconds. This is the sole mechanism by which enemy waves grow stronger over time. - REQ-WAV-SHIP-LEVEL: Each wave's enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where x is elapsed game time in seconds. This is the sole mechanism by which enemy waves grow stronger over time.
- REQ-WAV-THREAT-COST: Ships are spawned until the accumulated threat is exhausted. Each ship type has a threat cost defined as `ships.toml [ship.threat].cost_formula`. Because enemy ship level increases with time, threat cost per ship rises naturally over the course of the game. - REQ-WAV-THREAT-COST: Each ship type has a threat cost defined as `ships.toml [ship.threat].cost_formula`. Ships are selected one at a time per REQ-WAV-TRIGGER until no eligible blueprint's cost fits the remaining threat budget. Because enemy ship level increases with time, threat cost per ship rises naturally over the course of the game.
- REQ-WAV-SPAWN-DURATION: Ships in a wave are spawned one at a time over `world.toml [waves].spawn_duration_seconds`. - REQ-WAV-SPAWN-DURATION: Ships in a wave are spawned one at a time over `world.toml [waves].spawn_duration_seconds`.
- REQ-WAV-SHIP-STATS: Per-ship stats (damage: `[ship.combat].damage_formula`, attack rate: `[ship.combat].attack_rate_formula`, range: `[ship.combat].attack_range_formula`, speed: `[ship.movement].speed_formula`) and threat cost (`[ship.threat].cost_formula`) are each defined as formulas of ship level in `ships.toml`. - REQ-WAV-SHIP-STATS: Per-ship stats (damage: `[ship.combat].damage_formula`, attack rate: `[ship.combat].attack_rate_formula`, range: `[ship.combat].attack_range_formula`, speed: `[ship.movement].speed_formula`) and threat cost (`[ship.threat].cost_formula`) are each defined as formulas of ship level in `ships.toml`.
- REQ-WAV-GRACE-PERIOD: The grace period before the first wave is implicit: threat accumulates from t=0 but the first wave does not trigger until the initial gap (drawn at game start) has elapsed. - REQ-WAV-GRACE-PERIOD: The grace period before the first wave is implicit: threat accumulates from t=0 but the first wave does not trigger until the initial gap (drawn at game start) has elapsed.
@@ -177,7 +178,7 @@ The screen is divided into three vertical sections:
- REQ-UI-SCROLL: The player scrolls the view horizontally across the scrollable area by pressing A (scroll left) and D (scroll right). - REQ-UI-SCROLL: The player scrolls the view horizontally across the scrollable area by pressing A (scroll left) and D (scroll right).
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out. - REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
- REQ-UI-BLUEPRINT-TOAST: When a blueprint is unlocked or leveled up (REQ-DEF-BLUEPRINT-DROP), a transient notification toast appears in the top-right corner of the game world view for 4 seconds and then fades out. Toast text: - REQ-UI-BLUEPRINT-TOAST: When a blueprint is unlocked or leveled up (REQ-DEF-BLUEPRINT-DROP), a transient notification toast appears in the top-right corner of the game world view for 4 seconds and then fades out. `<Ship Name>` in the text below is the blueprint's `ships.toml [ship.blueprint].display_name`. Toast text:
- **New unlock**: `Blueprint unlocked: <Ship Name>` - **New unlock**: `Blueprint unlocked: <Ship Name>`
- **Level-up (duplicate drop)**: `<Ship Name> production level → N` (where N is the new level). - **Level-up (duplicate drop)**: `<Ship Name> production level → N` (where N is the new level).