diff --git a/docs/architecture.md b/docs/architecture.md index 49c507f..4cfeb1e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -14,7 +14,7 @@ This document captures the architectural decisions for the project. It is a comp A strict separation between the game simulation and the Qt Widgets UI. -- The **simulation** is a pure C++ library that depends only on Qt Core (QPoint, QVector3D, QRect, etc., as required by the coding guidelines), toml++, and tinyexpr. It contains no QtWidgets, no painting, and no QApplication. +- The **simulation** is a pure C++ library that depends only on Qt Core (QPoint, QVector2D, QRect, etc., as required by the coding guidelines), toml++, and tinyexpr. It contains no QtWidgets, no painting, and no QApplication. - The **UI** reads simulation state and renders it. It owns all widgets, painting, and input handling, and drives the simulation via a small command interface (place building, demolish, clear belt tiles, change recipe, set game speed, etc.). This split is enforced at the CMake target level (see below). Tests link only against the simulation library and run without a display server. @@ -23,7 +23,7 @@ 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. -- Tick rate: fixed (30 or 60 Hz, TBD; choose once and document here). +- Tick rate: fixed at 30 Hz. - The outer loop advances `N` ticks per wall-clock frame, where `N` is derived from the selected game speed: - 0× → 0 ticks/frame (pause) - 0.5× → one tick every two frames @@ -42,6 +42,44 @@ Config files (`world.toml`, `buildings.toml`, `recipes.toml`, `ships.toml`, `sta - Formula strings (e.g., threat accumulation, enemy ship level as a function of `t`, per-ship stats as functions of `level`, station stats as functions of generation) are compiled once via `tinyexpr` at load time and stored as callable objects. They are never re-parsed during simulation. - Configs are immutable after load. Any formula that fails to parse, or any required field that is missing or malformed, aborts startup with a clear error message — never mid-game. +## Coordinate System + +See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section captures the programming-level conventions that follow from it. + +- Tile coordinates are `QPoint(x, y)`. Origin `(0, 0)` is the first space tile (just right of the asteroid's right edge at game start). X grows right; Y grows down. +- Asteroid tiles have `x < 0`. Asteroid left-expansions add tiles at increasingly negative X; the origin never shifts, so existing tile coordinates remain stable across expansions. +- Continuous world positions (ship centers, scrap drops, projectiles) use `QVector2D` in tile units — one tile = 1.0 world unit. A ship center at `QVector2D(-3.5, 4.0)` sits at the center of the tile 3.5 tiles left of the asteroid's right edge and 4 tiles down from the top. +- Rendering multiplies world units by the tile size in pixels (20) at draw time. +- Ship position always refers to the ship's center — this is the point used for sensor, attack-range, and hit-detection checks. + +## Core Types + +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`. +- `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). +- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap). +- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks. +- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell. +- `MovementIntent` — `struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner. + +## 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). + +1. **Wave scheduler** — advance wave timer; on trigger, compute wave composition per REQ-WAV-TRIGGER and schedule spawn times across REQ-WAV-SPAWN-DURATION; spawn any enemy ships whose scheduled time has arrived this tick. +2. **Threat accumulation** — add `max(0, threat_rate_formula(t))` × tick_dt to threat level (REQ-WAV-THREAT-RATE). +3. **Belt → building pull** — buildings drain eligible items from adjacent belt tiles into per-material input buffers (REQ-MAT-INPUT-PORTS). +4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output. +5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT). +6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER). +7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority). +8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. +9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP), drop blueprints (REQ-DEF-BLUEPRINT-DROP), remove entities. +10. **`tickMovement`** — advance ship positions based on final `MovementIntent`. +11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP). + ## CMake Target Layout Three product targets plus tests: @@ -133,10 +171,10 @@ Behaviors are decomposed, not bundled into per-role monolithic AIs. This is the struct ThreatResponse { float engagementRange; CombatStance stance; CombatTargetPriority priority; std::optional currentTarget; }; -struct ScrapCollector { std::optional scrapTarget; EntityId deliveryBay; }; +struct ScrapCollector { std::optional scrapTarget; EntityId deliveryBay; }; struct RepairBehavior { RepairTargetPriority priority; std::optional currentTarget; }; -struct HomeReturn { float retreatHpFraction; QVector3D homePos; }; +struct HomeReturn { float retreatHpFraction; QVector2D homePos; }; ``` ### Ship @@ -144,8 +182,8 @@ struct HomeReturn { float retreatHpFraction; QVector3D homePos; }; ```cpp struct Ship { EntityId id; - QVector3D position; - QVector3D velocity; + QVector2D position; + QVector2D velocity; float hp; float maxHp; int level; diff --git a/docs/requirements.md b/docs/requirements.md index 9eddbba..cfaf71b 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -4,10 +4,10 @@ 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, enemy ship level formula. +- **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. - **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. -- **ships.toml** — ship stats (HP, speed, damage, attack range, attack rate) 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** — 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. - **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level. ### Surface Mask Format @@ -26,7 +26,9 @@ Output port indicators are not building tiles themselves. A building may have mo ## Game World +- REQ-GW-COORDS: Tile coordinates are integer `(x, y)`. The origin `(0, 0)` is the first column of space — the tile immediately to the right of the asteroid's right edge at game start, at the top of the world. X grows right; Y grows down. All asteroid tiles have `x < 0`; asteroid left-expansions add tiles at increasingly negative X. The origin never shifts. - REQ-GW-TILE-SIZE: Tiles are 20×20 pixels. Items on belts are 10×10 pixels (half a tile), so each belt tile holds at most 2 items. +- REQ-GW-BELT-SPEED: Items on belts move at `world.toml [world].belt_speed_tiles_per_second` tiles per second (default 2). - REQ-GW-HEIGHT: The world height (in tiles) is read from `world.toml [world].height_tiles`. - REQ-GW-REGIONS: The world is divided into horizontal regions whose widths (in tiles) are read from `world.toml [regions]`: - **Asteroid** — the player's build area (`asteroid_width`). @@ -42,31 +44,31 @@ Output port indicators are not building tiles themselves. A building may have mo - REQ-HQ-PLACEMENT: The HQ is pre-placed at the asteroid's right edge at game start. - REQ-HQ-BELT-INPUT: The HQ has a belt input port. Building blocks delivered to it are added to the global building blocks stock. - REQ-HQ-STATS: HQ stats (HP) are read from `stations.toml [hq]`. -- REQ-HQ-GAME-OVER: If the HQ is destroyed, the game ends and the final survival time is shown. +- REQ-HQ-STARTING-BLOCKS: At game start, the global building blocks stock is initialized to `world.toml [world].starting_building_blocks` (default 100). +- REQ-HQ-GAME-OVER: If the HQ is destroyed, the game ends. A game-over screen shows the final survival time and offers "Restart" and "Quit" buttons. - REQ-HQ-INVULNERABLE: Factory buildings (other than the HQ) are never targeted or destroyed by enemies. ## Building Placement & Management - REQ-BLD-COST: The player places buildings from a build menu. Placement costs building blocks from the global stock. The cost per building type is read from `buildings.toml [[building]].cost`. - REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct. -- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles. -- REQ-BLD-SHIPYARD-EDGE: Shipyards must be placed at the asteroid's right edge. +- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space). - REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by pressing Escape, right-clicking in the game world, or clicking the same build button again. - 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-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-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 building. Demolition returns `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) to the global stock. +- 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. ## Building Types -- REQ-BLD-MINER: **Miner** (2×2): The player selects which ore type it extracts. Each ore type corresponds to a `recipes.toml [[recipe]]` entry with `building = "miner"`, defining the output item and `duration_seconds`. Ore never depletes. +- REQ-BLD-MINER: **Miner** (2×2): The player selects which ore type it extracts. Each ore type corresponds to a `recipes.toml [[recipe]]` entry with `building = "miner"`, defining the output item and `duration_seconds`. Every asteroid tile is equivalent for mining — any miner can produce any ore type based solely on its selected recipe. Ore never depletes. - 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-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces higher-level intermediate products. The input quantity, output items, and per-output drop probabilities are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].amount`, `outputs[].probability`). +- 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-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. Available in straight and curved variants. +- 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: - An item matching only one output's filter is routed to that output. - An item matching both outputs' filters is distributed by strict alternation between those outputs. @@ -77,10 +79,12 @@ Output port indicators are not building tiles themselves. A building may have mo ## Material Transport & Buffers - REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts and splitters. -- REQ-MAT-INPUT-PORTS: A building accepts items from all adjacent belt tiles whose direction points toward the building, provided the item is an input required by the currently selected recipe. -- REQ-MAT-OUTPUT-PORT: Each building has one fixed output port (direction determined by rotation). Produced items are placed onto the belt at the output port. -- REQ-MAT-INPUT-BUFFER: Each building's input buffer holds up to twice the quantity of each required input material for one production cycle. When the player selects a new recipe or blueprint, all items in the input buffer are cleared. -- REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. +- REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space. +- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items are placed onto the belt at the output port. +- REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or blueprint, all items in all input buffers are cleared. +- REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or blueprint, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated). +- REQ-MAT-OUTPUT-BUFFER-REPROCESSING: Exception to REQ-MAT-OUTPUT-BUFFER — the Reprocessing Plant's output buffer holds at most one cycle's output. This prevents exploits where the player stalls the output belt to force the plant to reroll. +- REQ-MAT-CYCLE: Production cycle lifecycle. When a building is idle, it attempts to start a new cycle: (a) all required inputs must be present in the per-material input buffers, and (b) the cycle's output must fit in the output buffer. For the Reprocessing Plant, the output is picked at cycle start (weighted pick); the cycle only starts if that chosen output fits. On cycle start, inputs are consumed immediately and the production timer begins. On cycle completion, the (already-decided) output is deposited into the output buffer and the building returns to idle. - REQ-MAT-GLOBAL-STOCK: The building blocks stock is the only global inventory. All other materials exist only in building buffers or on belt tiles. ## Resources @@ -94,13 +98,18 @@ Output port indicators are not building tiles themselves. A building may have mo ## Ships - REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced. -- REQ-SHP-STATS: All ship stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), speed (`[ship.movement].speed_formula`), damage (`[ship.combat].damage_formula`), attack range (`[ship.combat].attack_range_formula`), attack rate (`[ship.combat].attack_rate_formula`). Required build materials (`[ship.blueprint].materials`) and availability from game start (`[[ship]].available_from_start`) are also defined there. -- REQ-SHP-NO-COLLISION: Ships move independently with no collision between them. +- REQ-SHP-STATS: All ship stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), speed (`[ship.movement].speed_formula`), damage (`[ship.combat].damage_formula`), attack range (`[ship.combat].attack_range_formula`), attack rate (`[ship.combat].attack_rate_formula`), sensor range (`[ship.sensors].range_formula`). Required build materials (`[ship.blueprint].materials`) and availability from game start (`[[ship]].available_from_start`) are also defined there. +- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile. +- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height. +- REQ-SHP-MOVEMENT: Ships move in straight lines toward their current destination at the speed defined by their speed formula. Ship position refers to the ship's center for all range, sensor, and attack checks. +- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap. +- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored. +- REQ-SHP-ENEMY-AI: Enemy ships engage the closest valid target (player defence station, HQ, or player ship) within their sensor range. If no target is in sensor range, they move toward the asteroid (leftward in world coordinates). - REQ-SHP-COMBAT: **Combat ships** — move right through space and engage enemy ships. The player can configure the following per shipyard (applied to all ships produced by that shipyard): - Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid). - Target priority: closest / highest HP / structures first. -- REQ-SHP-SALVAGE: **Salvage ships** — move to the location of destroyed enemy ships, collect scrap, and deliver it to a Salvage Bay on the asteroid. Vulnerable to enemy ships while operating. -- REQ-SHP-REPAIR: **Repair ships** — move to damaged player defence stations or player ships and repair them. The player can configure the target priority per shipyard: +- 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: - 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. @@ -112,13 +121,14 @@ Output port indicators are not building tiles themselves. A building may have mo - REQ-DEF-ENEMY-PLACEMENT: 2 enemy defence stations are placed at the right boundary of the scrollable area at game start, and again each time a new set is spawned after a push. Stats scale with the station level (REQ-PSH-STATION-STATS). - REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range. - REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range. -- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the push scaling multiplier is applied (REQ-PSH-ACCUMULATION), the scrollable area is extended (REQ-GW-PUSH-EXPAND), and a new set of enemy defence stations is placed at the new boundary. The destroyed stations drop ship blueprint loot. +- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the push scaling multiplier is applied (REQ-PSH-ACCUMULATION), the scrollable area is extended (REQ-GW-PUSH-EXPAND), and a new set of enemy defence stations is placed at the new boundary. The destroyed stations drop ship blueprint loot (REQ-DEF-BLUEPRINT-DROP). +- REQ-DEF-BLUEPRINT-DROP: One ship blueprint is chosen uniformly at random from all blueprints defined in `ships.toml`. If the player does not yet have that blueprint, it is unlocked. If the player already has it, the blueprint's `[ship.blueprint].player_production_level` is incremented by 1 — so subsequent ships of that type are produced at a higher level. ## Threat Level & Enemy Waves -- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where x is elapsed game time in seconds. Example: `1*x - 30` gives 0 threat/s at x=30s and increases linearly after that. +- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where x is elapsed game time in seconds, clamped to a minimum of 0 (negative formula values are treated as 0). Example: `1*x - 30` yields 0 threat/s for x ≤ 30s and increases linearly after that. - 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 spawned using the accumulated threat level. Any threat not spent on ships 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-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-SPAWN-DURATION: Ships in a wave are spawned one at a time over `world.toml [waves].spawn_duration_seconds`. @@ -163,6 +173,14 @@ The screen is divided into three vertical sections: ### Game World - 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-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×). + - **S** — decreases game speed by one step in the same sequence (no wrap-around past 0×). + - **Backspace** — activates demolish mode; Escape or Backspace again exits it. + - **Q / E** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE). + - **Escape** — exits builder mode or demolish mode. ### Selected Building Panel