# Requirements ## Config Files 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, belt speed, starting building blocks, departure interval. - **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** — per schematic: 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 schematic 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. - **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs 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. - REQ-CFG-RELOAD: When the player triggers a Restart (REQ-UI-GAME-MENU), all config files are reloaded from disk before the simulation is reset to its initial state. Formula strings are recompiled at that point. This allows config edits made while the application is running to take effect without a full application restart. ### Surface Mask Format Buildings in buildings.toml define a `surface_mask` — a list of strings that describes the building's tile footprint and output port(s). Each character occupies one cell in the grid: - `A` — building tile that must be placed on an asteroid tile. - `S` — building tile that must be placed on a space tile. - ` ` (space) — empty cell; not part of the building footprint. - `>` — output port indicator: the building tile immediately to its left has a rightward-facing output port (items push right). - `<` — output port indicator: the building tile immediately to its right has a leftward-facing output port (items push left). - `^` — output port indicator: the building tile immediately below it has an upward-facing output port (items push up). - `v` — output port indicator: the building tile immediately above it has a downward-facing output port (items push down). Output port indicators are not building tiles themselves. A building may have more than one output port (e.g. a splitter uses `` to declare both a left and a right output on the same tile). ## 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 square. The tile size in pixels is derived automatically so that the world height (in tiles) exactly fills the game world view's height in pixels. Items on belts are rendered at half-tile size, 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`). - **Player buffer zone** — space between the asteroid and the player's defence stations (`player_buffer_width`). - **Contest zone** — space between the player's and enemy's defence stations. This is where combat happens (`contest_zone_width`). - **Enemy buffer zone** — space between the enemy's defence stations and the enemy spawn boundary (`enemy_buffer_width`). - REQ-GW-SCROLL-LIMIT: The player can scroll the view horizontally from the asteroid's left edge to the current set of enemy defence stations (the enemy buffer zone is not visible). - REQ-GW-PUSH-EXPAND: When the player destroys a set of enemy defence stations, the scrollable area is extended by `world.toml [push].push_expand_columns` tiles. A new enemy buffer zone of `world.toml [regions].enemy_buffer_width` tiles is added beyond the new enemy defence stations. - REQ-GW-ASTEROID-EXPAND: When the player unlocks an asteroid expansion, `world.toml [expansion].columns_per_expansion` tile columns are added to the left of the asteroid. ## HQ & Game Over - 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-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 (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 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-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 — (b) no footprint cell overlaps an existing placed building or construction site, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color when the current cursor position fails any of these conditions. - 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. Exception: if the building is still in the construction queue (not yet fully built, including the one currently being constructed), it is removed from the queue and the **full** building block cost is refunded. 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`. 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 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 schematic. When all required materials (`[ship.schematic].materials`) are present in its input buffer, the shipyard consumes them and begins a production cycle lasting `[ship.schematic].production_time_seconds` seconds (read from `ships.toml`). One ship of that type is spawned at `ships.toml [ship.schematic].player_production_level` (initial value 5, incremented by duplicate schematic drops per REQ-DEF-SCHEMATIC-DROP) when the cycle completes. The shipyard cannot start a new cycle while one is in progress. - 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-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. - An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved. - If neither output has a filter, items are distributed by strict alternation. - In all alternation cases, if one output is blocked the item goes to the other output until it unblocks. - REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with Q/E. Items arriving from an adjacent belt tile whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile. - REQ-BLD-TUNNEL-EXIT: **Tunnel Exit** (1×1): The receiving end of a tunnel pair. The player sets a direction at placement, rotatable with Q/E. Items received from the paired Tunnel Entry emerge from the output side of the exit tile — the tile adjacent in the exit's facing direction — continuing in that direction. If the exit is unpaired or its output is blocked, it holds received items until they can advance. - REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed or demolished. - A Tunnel Entry searches tile-by-tile in its facing direction for a partner. Any tunnel building (entry or exit) that faces a *different* direction is ignored and skipped. The search stops at the first tunnel building that faces the *same* direction as the searching entry. - If that first same-direction tunnel building is a Tunnel Exit, is within `tunnel_max_distance` tiles of the entry, and is not already paired with a closer entry, the two form a pair. - Otherwise the entry is unpaired. - Pairing is one-to-one: each Tunnel Entry pairs with at most one Tunnel Exit, and vice versa. A Tunnel Exit is claimed by the nearest Tunnel Entry that can validly reach it; all other entries for which it would otherwise qualify are unpaired. - When one end of a pair is demolished, the pair is dissolved and any items currently in transit are discarded. - REQ-BLD-TUNNEL-TRANSIT: **Tunnel transit.** Items inside a tunnel are not rendered (they travel invisibly). Transit time equals the tile-coordinate distance between entry and exit divided by `world.toml [world].belt_speed_tiles_per_second`, matching the time a chain of belt tiles of equivalent length would take. Multiple items may be in transit simultaneously, spaced as they would be on a belt chain of the same length. Clearing a tunnel entry or exit tile (REQ-UI-BELT-CLEAR) also discards all items currently in transit through that tunnel. ## Material Transport & Buffers - REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels. - 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 tile regardless of that belt's direction. - 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 schematic, 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 schematic, 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 - REQ-RES-SCRAP-DROP: Destroyed ships (both player and enemy) and destroyed defence stations (both player and enemy) drop scrap at their location. The scrap amount per ship is defined in `ships.toml [ship.loot].scrap_drop`; for stations it is defined as `stations.toml [player_station].scrap_drop_formula` and `[enemy_station].scrap_drop_formula`. Scrap despawns after `world.toml [world].scrap_despawn_seconds` seconds if not collected. - REQ-RES-SCRAP-COLLECT: Scrap is collected by salvage ships and delivered to a Salvage Bay on the asteroid. From there it can be fed via belt into a smelter (same output as ore) or a Reprocessing Plant. ## 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`), sensor range (`[ship.sensors].range_formula`). Required build materials (`[ship.schematic].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-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application. - REQ-SHP-FIRING-BEAM: Each fire event produces a visual laser beam drawn from the shooter's position to the target's position for 0.3 seconds. The beam is a pure rendering effect and has no simulation state (does not block movement, does not re-apply damage over its lifetime). Beams follow the shooter and target positions if either moves during the 0.3-second window. The beam is rendered for its full 0.3-second duration even if the shooter or target is destroyed before it expires. - REQ-SHP-COMBAT: **Combat ships** (player) — engage enemy ships within sensor range. 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-RALLY: After spawning, aggressive-stance combat ships move to and loiter at the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position). While at the rally point, ships still engage any enemy that enters sensor range. Every `world.toml [world].departure_interval_seconds` seconds (default 20), all combat ships currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point. - REQ-SHP-SALVAGE: **Salvage ships** (player) — 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** (player) — 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-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-SCHEMATICS: The player selects a schematic per shipyard by clicking it. New schematics are unlocked automatically when an enemy defence station set is destroyed (REQ-DEF-SCHEMATIC-DROP) — there is no physical loot to collect. ## Defence Stations - REQ-DEF-PLAYER-PLACEMENT: 2 player defence stations are pre-placed in space at the start. Their positions are determined by `world.toml [regions].asteroid_width` and `player_buffer_width`. - REQ-DEF-PLAYER-FIRE: Player defence stations automatically fire at approaching enemy ships. Stats are defined as formulas of a fixed station level (`stations.toml [player_station].level`) in `stations.toml [player_station]`: `hp_formula`, `damage_formula`, `range_formula`, `fire_rate_formula`, `scrap_drop_formula`. - REQ-DEF-PLAYER-DESTRUCTIBLE: Player defence stations can be destroyed by enemies and repaired by repair ships. - 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), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP). - REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop is automatic — no physical item to collect. A schematic is chosen uniformly at random from all schematics defined in `ships.toml`. If the player does not yet have that schematic, it is unlocked. If the player already has it, the schematic's `[ship.schematic].player_production_level` is incremented by 1 — so subsequent ships of that type are produced at a higher level. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST). ## 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, 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 selected one at a time: from all schematics 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 schematic fits. Any remaining threat carries over to the next wave. A longer gap results in a larger wave. Because enemy ship level increases with time (REQ-WAV-SHIP-LEVEL), threat cost per ship rises naturally over the course of the game. - 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 individual enemy ships become stronger over time. Wave *size* grows separately via threat accumulation (REQ-WAV-THREAT-RATE) and push scaling (REQ-PSH-ACCUMULATION). Per-ship stats and threat cost are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS). - REQ-WAV-SPAWN-DURATION: Ships in a wave are spawned one at a time over `world.toml [waves].spawn_duration_seconds`. ## Push Scaling - REQ-PSH-ACCUMULATION: Each time the player destroys a set of enemy defence stations, `world.toml [push].scaling_factor` is multiplied permanently into the threat level accumulation rate. Scaling factors stack multiplicatively with each other and with the time-based threat formula, causing all subsequent waves to be larger. - REQ-PSH-STATION-STATS: Enemy defence station stats are each defined as formulas in `stations.toml [enemy_station]`: `hp_formula`, `damage_formula`, `range_formula`, `fire_rate_formula`, `scrap_drop_formula`, where x is the station level — an integer starting at 0 for the initial set and incrementing by 1 each time a new set is placed. ## Asteroid Expansion - REQ-EXP-UNLOCK: The player can unlock additional asteroid tile columns to the left of the existing asteroid by spending building blocks from the global stock. - REQ-EXP-COST: Each expansion adds `world.toml [expansion].columns_per_expansion` columns and costs `[expansion].cost_building_blocks` building blocks. ## UI ### Layout The screen is divided into three vertical sections: ``` +--------------------------------------------------+ | Header Bar | +--------------------------------------------------+ | | | Game World (70%) | | | +-----------------+-----------------+--------------+ | Selected | Build Button | Blueprint | | Building Panel | Grid | Panel | | (left) | (center) | (right) | +-----------------+-----------------+--------------+ ``` - REQ-UI-HEADER: The header bar spans the full width above the game world and always shows the elapsed survival time and the current global building blocks stock on the left, and game speed controls on the right. - 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-WORLD-HEIGHT: The game world view occupies 70% of the remaining screen height below the header bar. - REQ-UI-PANEL-HEIGHT: The UI panel occupies the remaining 30% of the screen height, split horizontally into a selected building panel (left), a build button grid (center), and a blueprint panel (right). ### 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-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-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out. - REQ-UI-SCHEMATIC-TOAST: When a schematic is unlocked or leveled up (REQ-DEF-SCHEMATIC-DROP), a transient notification toast appears in the top-right corner of the game world view for 4 seconds and then fades out. `` in the text below is the schematic's `ships.toml [ship.schematic].display_name`. Toast text: - **New unlock**: `Schematic unlocked: ` - **Level-up (duplicate drop)**: ` production level → N` (where N is the new level). If multiple toasts arrive in close succession, they stack vertically in a queue (most recent at the top) and each fades out independently after its own 4-second lifetime. - 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; Backspace again exits it. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.) - **Q / E** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE). - **Escape** — opens the escape menu (REQ-UI-GAME-MENU). - **M** — toggles debug draw mode (REQ-UI-DEBUG-DRAW). ### Debug Draw - REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **M** key (REQ-UI-HOTKEYS). It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship role's outline color from `visuals.toml`. ### Escape Menu - REQ-UI-GAME-MENU: Pressing Escape at any time opens the escape menu as a modal dialog and pauses the simulation (sets speed to 0×). On close, the simulation speed is restored to what it was before the menu was opened — so if the game was already paused, it remains paused. The menu contains three buttons: - **Continue** — closes the menu and returns to the game. - **Restart** — resets the simulation to its initial state and closes the menu at 1× speed. - **Quit** — closes the application. Pressing Escape while the escape menu is open is equivalent to clicking Continue. ### Selected Building Panel - REQ-UI-EMPTY-SELECTION: When no building is selected, the panel is empty. - REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). - REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown. - REQ-UI-MULTI-SELECT: The player selects multiple buildings by box-drag or by Ctrl+clicking individual buildings to add or remove them from the selection. - REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. - REQ-UI-CONFIG-INLINE: Recipe, schematic, ship stance, and target priority configuration for a selected building is shown and changed inline within this panel. - REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. ### Build Button Grid - REQ-UI-BUILD-GRID: All placeable building types are shown as a flat grid of buttons with no grouping. - REQ-UI-BUILD-COST: Each button caption shows the building name and its building block cost, e.g. "Belt: 2 Blocks". - REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled. - REQ-UI-DEMOLISH-BUTTON: A dedicated **Demolish** button is shown in the build button grid. Clicking it toggles demolish mode on and off, equivalent to pressing Backspace (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while demolish mode is active. ### Blueprint Panel - REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, a "Delete Blueprint" button, and a list of blueprint buttons (one per saved blueprint, in creation order). - REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list. - REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards) at the time of capture. If no recipe or schematic was selected at capture time, none is stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building). - REQ-UI-BLUEPRINT-BUTTON: Each blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. - REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint. - REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately. - REQ-UI-BLUEPRINT-DELETE: Clicking the "Delete Blueprint" button toggles delete mode on and off. While delete mode is active the button is shown in a visually active/pressed state. Clicking any blueprint button while delete mode is active removes that blueprint from the list and automatically exits delete mode.