Procedural Generation — Storey Layout Reference¶
Document Type: Reference Last Updated: 2026-03-21 Vault destination:
05_Reference
Auto Warz — Outpost Mode Last updated: 2026-03-16
What This Document Is¶
A living reference for the storey layout generator. Written after the first working implementation so the reasoning behind every parameter is captured while it's fresh. Update this whenever a parameter changes or a new system is added.
Overview — How a Storey Gets Made¶
The generator runs five passes in sequence. Each pass takes the current state of the layout and adds to it. No pass looks backward — each one builds on what came before.
Pass 1 — Dimensions Set grid width and height from depth + level + StoreyType
Pass 2a — BSP Rooms Carve rooms and OUT_OF_BOUNDS chunks
Pass 2b — Entry/Trigger Place ENTRY zone and TRIGGER before corridors
Pass 2c — Corridors Carve corridors toward TRIGGER (known position)
Pass 3 — Shaft/Stair Place SHAFT cells and STAIRCASE
Pass 4 — Validation A* confirms enemy path exists (read-only)
Pass 5 — Annotation BFS open zone count
The result is a StoreyLayout resource — a pure data object with no scene
nodes. The renderer reads it separately. The generator never touches the
scene tree.
Critical architecture rule: ENTRY and TRIGGER are placed in Pass 2b, BEFORE corridor carving in Pass 2c. This guarantees corridors route toward TRIGGER and it is never isolated. Do not move this order.
The Grid¶
Every storey is a 2D grid of cells. The grid has a bounding rectangle (width × height). Not every cell inside the bounding rect is playable — some are OUT_OF_BOUNDS, which creates the irregular outer shape.
Grid coordinates: (0,0) = top-left, (width-1, height-1) = bottom-right
World position of cell (x, y) = (x * CELL_SIZE, 0, y * CELL_SIZE)
Cell centre = (x * CELL_SIZE + 0.5, 0, y * CELL_SIZE + 0.5)
CELL_SIZE = 1.0 world units
Cell Types¶
| Type | Value | Meaning | Rendered as |
|---|---|---|---|
| OUT_OF_BOUNDS | 0 | Outside playable shape | Nothing — no mesh |
| WALL | 1 | Solid — blocks movement | Tall box (height 2.0) |
| EMPTY | 2 | Open floor — walkable, buildable | Flat tile (height 0.1) |
| ENTRY | 3 | Enemy spawn point — discrete opening in outer wall | Green floor tile |
| BEACON | 4 | Communications restoration point | Vivid magenta glowing pillar |
| SHAFT | 5 | Tube connection point — exactly 2 per storey | Blue floor tile |
| STAIRCASE | 6 | Up/down transition point | Amber floor tile |
| PATH | 7 | Not stored in cells — overlay on enemy_path | Yellow floor tile |
Walkable cells (Genesis and enemies can traverse): EMPTY, ENTRY, BEACON, SHAFT, STAIRCASE
Non-walkable cells: OUT_OF_BOUNDS, WALL
PATH overlay note: The renderer skips ENTRY, BEACON, and STAIRCASE cells when drawing the yellow path overlay — their own colours remain visible so special cells are always identifiable even when on the path.
BEACON notes: - Represents damaged communications infrastructure Genesis must restore - Multiple BEACONs per storey — count scales with storey depth - First BEACON always placed in a dead end or corner cell - Additional BEACONs placed in interior cells at minimum separation distance - Enemies attack BEACON cells — causes bleed if damaged - Each BEACON contributes independently to storey yield (partial progress) - All BEACONs on all storeys restored → RECLAIM BUILDING becomes available
StoreyType¶
Each storey has a type that affects its dimensions, BSP behaviour, and
corridor width. Set via the StoreyType enum in StoreyLayout.gd.
| Type | Meaning |
|---|---|
| GROUND_FLOOR | First floor of a building — medium size, standard corridors |
| UPPER_FLOOR | Higher floors — slightly smaller, wider corridors, no OOB cuts |
| UNDERGROUND | Factory floor — larger OOB probability, narrower corridors |
StoreyType Effects on Generation¶
| Parameter | GROUND_FLOOR | UPPER_FLOOR | UNDERGROUND |
|---|---|---|---|
| OOB probability | 35% | 0% | 55% |
| Corridor width | 2 cells | 2 cells | 1 cell |
| Dimension adjustment | base −2w −1h | base −2w −1h | base (full size) |
Why UPPER_FLOOR has 0% OOB: Upper floors are contained inside a building. They should fill their footprint — irregular outer shape comes from the building's outer walls, not from missing floor sections.
Why UNDERGROUND has 55% OOB and 1-cell corridors: The underground factory floor needs large open spaces for building placement, but the overall shape should be more irregular and cave-like. Narrower corridors preserve wall mass for factory context.
Architecture — Cells vs Objects vs Assets¶
Three distinct layers handle what appears in a storey. Each layer has a single responsibility and must not be mixed with the others.
Layer 1 — Cell Types (pure data)¶
Cell types are integers stored in the grid. They define what a cell IS — not how it looks or how it behaves. Never add visual or gameplay logic to cell type values.
Layer 2 — StoreyRenderer (visuals)¶
Reads cell types and spawns mesh nodes. Currently uses procedural BoxMesh with hardcoded colours — placeholder only.
Art pass upgrade path: data/outpost/cell_definitions.tres — one resource per cell type references .glb mesh and material StoreyRenderer reads cell_definitions.tres instead of hardcoded values Different outpost themes (rusty industrial, clean tech, alien organic) = different cell_definitions.tres, zero code changes
Layer 3 — StoreyObjectSpawner (interactive objects)¶
Reads the layout AFTER rendering and spawns scene-based interactive objects at special cell positions. Each special cell type gets its own .tscn scene with state, signals, and interaction logic.
BEACON cell → spawns BeaconObject.tscn (health, restoration progress, damage state, particles) ENTRY cell → spawns EntryPoint.tscn (drone spawner, spawn rate, wave profile reader) STAIRCASE cell → spawns StaircaseObject.tscn (interaction zone, up/down prompt) SHAFT cell → spawns ShaftConnector.tscn (tube connection point, throughput state)
StoreyObjectSpawner is implemented for BEACON and ENTRY cells. STAIRCASE and SHAFT object scene spawning is still pending.
Rule¶
Cell type = what kind of cell is this (integer, never changes) Renderer = how the cell looks (mesh, material, asset-driven) Object = what the cell does (scene instance, state, signals)
Never put gameplay logic in StoreyRenderer. Never put visual logic in cell type definitions. Never skip StoreyObjectSpawner by putting interaction logic directly in StoreyRenderer.
Pass 1 — Dimensions¶
Computes width and height from storey_depth, outpost_level,
and storey_type.
storey_depth: 1-based index of this storey within its building.
Deeper storeys = larger, more complex layouts.
outpost_level: 1-4. Higher levels = larger layouts overall.
storey_type: Applies dimension adjustment on top of base calculation.
Bounds:
All cells initialised to OUT_OF_BOUNDS at the end of Pass 1.
BSP (Pass 2a) carves the playable shape out of this blank slate.
Why these bounds: - 16×10 minimum gives enough space for meaningful BSP splits (at least 12 leaves with _MIN_PARTITION = 3) - 28×20 maximum keeps generation fast and the layout readable - Going below 16 wide produces too few leaves and rectangular shapes
Pass 2a — BSP Room Creation¶
Splits the bounding rect recursively. Marks some perimeter partitions as OOB. Converts the rest to rooms filled with EMPTY cells.
Key Parameter: _MIN_PARTITION¶
This is the most important parameter in the generator.
Minimum cell count on each axis before stopping splits.
| Value | Leaves from 16×12 | Result |
|---|---|---|
| 6 | ~6 leaves | Too few — rectangular, no OOB cuts fire |
| 4 | ~8-10 leaves | Better but still boxy |
| 3 | ~12-20 leaves | Good — enough variety and OOB opportunity |
| 2 | ~20-30 leaves | Too many — tiny rooms, maze-like |
Set to 3. Do not raise above 4 — you will get rectangular outputs.
OOB Decision¶
if not is_edge_required and is_perimeter and rng.randf() < oob_probability:
continue # entire partition stays OUT_OF_BOUNDS
oob_probability comes from StoreyType (35% / 0% / 55%).
Protected partitions (never OOB): - Leftmost two — needed for ENTRY zone width - Rightmost one — needed for TRIGGER placement
Room Inset: _ROOM_SHRINK¶
Each room is inset 1 cell from its partition boundary. Border cells become WALL. Do not set to 0 (rooms merge) or 2+ (rooms too small).
Pass 2b — Entry and Beacon Placement¶
ENTRY Points¶
Discrete openings in the outer wall where drones enter the storey. Not a zone — individual cells placed on the perimeter.
Count scales with storey depth: depth 1 → 1 ENTRY depth 2-3 → 2 ENTRYs depth 4+ → 3 ENTRYs
Placement rules: - Must be adjacent to OUT_OF_BOUNDS or bounding rect edge - Can appear on any side — not just the left edge - Seeded by RNG — direction of threat varies per storey
Minimum distances: From any BEACON: 6 cells (_MIN_ENTRY_BEACON_DIST = 6) From each other: 5 cells (_MIN_ENTRY_SEPARATION = 5) From STAIRCASE: 4 cells (_MIN_ENTRY_STAIR_DIST = 4)
BEACON Cells¶
Number of BEACONs scales with storey depth:
| Storey depth | BEACON count |
|---|---|
| 1 | 1 |
| 2-3 | 2 |
| 4+ | 3 |
First BEACON placement — dead end or corner: A dead end cell has exactly 1 walkable neighbour. A corner cell is at the corner of a room (two non-walkable sides adjacent). The generator prefers dead ends, falls back to corners, falls back to any interior cell if neither exists.
Additional BEACONs — interior cells:
Placed in any interior EMPTY cell, minimum _MIN_BEACON_SEPARATION
cells apart from each other and from the first BEACON.
Corridor routing: Corridors in Pass 2c carve toward the FIRST BEACON only. Additional BEACONs are placed in rooms already connected by corridors so they are reachable without additional carving.
Pass 2c — Corridor Carving¶
Connects consecutive rooms in BSP sort order (left to right by X midpoint) with corridors. Then carves a guaranteed connection from the nearest room centre to TRIGGER.
Corridor width by StoreyType: - GROUND_FLOOR / UPPER_FLOOR: 2 cells wide - UNDERGROUND: 1 cell wide
Trigger connection uses single-cell width (_carve_line_h/v
not _carve_band_h/v). This matches TRIGGER's 1-cell size and
avoids open wall gaps at the trigger end. Using band carvers for
the trigger connection produced visual artefacts — gaps in wall
mass around the TRIGGER cell.
Pass 3 — Shaft and Staircase Placement¶
SHAFT Cells (exactly 2)¶
Placed on wall-border cells (adjacent to a WALL cell).
Must be at least _MIN_SHAFT_SEPARATION apart.
Placing shafts on wall-border cells means they sit at room edges — more realistic tube entry points than open floor centre positions.
STAIRCASE Cell¶
Single interior cell, not overlapping shaft cells.
Forced staircase support:
When forced_staircase is a valid position (not -1,-1), the
generator places STAIRCASE at that exact cell. Used when generating
multi-storey buildings — pass storey 1's staircase position to all
subsequent storeys so the staircase aligns vertically.
# Multi-storey chaining example
var s1 := StoreyGenerator.generate(master_seed ^ hash(0), 1, level)
var s2 := StoreyGenerator.generate(master_seed ^ hash(1), 2, level, s1.staircase_cell)
var s3 := StoreyGenerator.generate(master_seed ^ hash(2), 3, level, s1.staircase_cell)
Pass 4 — Path Validation¶
Runs A from every ENTRY cell to the TRIGGER cell.
Implemented in StoreyValidator.gd — read-only, never modifies layout.*
If no path found after MAX_RETRIES — push_warning, leave enemy_path empty. Pass 4 does not carve corridors or modify any cells. All layout work happens in passes 1-3. If validation fails it is a generator bug upstream.
A* Implementation Notes¶
- 4-directional movement only (no diagonals)
- Manhattan distance heuristic:
abs(dx) + abs(dy) - Walkable = EMPTY, ENTRY, TRIGGER, SHAFT, STAIRCASE
- WALL and OUT_OF_BOUNDS are impassable
- Implemented directly on StoreyLayout — no Godot pathfinding nodes
Pass 5 — Annotation¶
BFS flood fill counts distinct open floor regions.
Result stored as layout.open_zone_count.
open_zone_count = 1 — healthy, one connected playable area.
open_zone_count > 1 — disconnected regions exist, corridor carving
missed a connection somewhere.
StoreyLayout — Full Field Reference¶
| Field | Type | Set by | Meaning |
|---|---|---|---|
| seed_value | int | Pass 1 | The seed used to generate this layout |
| storey_type | StoreyType | Pass 1 | GROUND_FLOOR / UPPER_FLOOR / UNDERGROUND |
| width | int | Pass 1 | Grid width in cells |
| height | int | Pass 1 | Grid height in cells |
| cells | Array | Pass 2a | 2D grid — cells[x][y] = CellType int |
| entry_cells | Array[Vector2i] | Pass 2b | Discrete ENTRY point positions — 1 to 3 depending on storey depth |
| beacon_cells | Array[Vector2i] | Pass 2b | All BEACON positions — 1 to 3 depending on depth |
| primary_beacon | Vector2i | Pass 2b | First BEACON — always in dead end or corner |
| shaft_cells | Array[Vector2i] | Pass 3 | Exactly 2 SHAFT positions |
| staircase_cell | Vector2i | Pass 3 | The single STAIRCASE position |
| enemy_path | Array[Vector2i] | Pass 4 | A* path ENTRY→TRIGGER, empty if none |
| open_zone_count | int | Pass 5 | Number of distinct walkable regions |
Determinism Guarantee¶
Rules that must not be broken:
- Never use global randf() or randi() — always use the local rng
- Never add or remove RNG calls without testing determinism
- Never change the order of RNG calls between runs
- After any generator change, verify: generate the same seed 5 times
and confirm identical output every time
Renderer — Colour Reference¶
| Cell type | Colour | Hex approx |
|---|---|---|
| EMPTY floor | Light blue | #87CEEB |
| WALL | Warm grey | #59524A |
| ENTRY | Orange | #E88719 |
| BEACON | Vivid magenta | #FF4DCC |
| SHAFT | Blue | #335799 |
| STAIRCASE | Amber | #8C6619 |
| PATH overlay | Yellow | #B3A61A |
| OUT_OF_BOUNDS | (nothing) | — |
Debug Inspector — Parameter Guide¶
| Control | Range | Effect |
|---|---|---|
| Seed | Any int | Changes layout completely — same seed = same layout |
| Storey Depth | 1–6 | Higher = larger grid, more complex BSP |
| Outpost Level | 1–4 | Higher = larger grid scaling multiplier |
| StoreyType | Dropdown | GROUND_FLOOR / UPPER_FLOOR / UNDERGROUND |
Entry point from main game:
Main menu has a [DEV] Outpost Debug button → loads outpost_test.tscn.
Auto-hidden in release exports via OS.is_debug_build().
Bug Fix History¶
| Date | Bug | Root Cause | Fix |
|---|---|---|---|
| 2026-03-16 | OOB always 0, rectangular output | _MIN_PARTITION = 6 — only 6 leaves, 30% OOB never fired on non-protected partitions |
Changed to 3, raised OOB probability to 45% (then per-StoreyType) |
| 2026-03-16 | Enemy path always NONE | TRIGGER placed after corridor carving — corridors never routed toward it | Split passes: ENTRY/BEACON in Pass 2b before corridors in Pass 2c |
| 2026-03-16 | Visual/validator mismatch | Corridor carving set cells to EMPTY but validator read stale data | Fixed cell state sync between carver and validator |
| 2026-03-16 | Open wall gaps at beacon end | _carve_band_h/v (2-cell wide) used for beacon connection — width mismatch |
Changed beacon connection to _carve_line_h/v (1-cell wide) |
| 2026-03-16 | Staircase overwritten by path overlay | Renderer PATH overlay wrote yellow over STAIRCASE cells | _overlay_path() now skips STAIRCASE, ENTRY, and BEACON |
| 2026-03-21 | ENTRY zone redesigned | Full left-column zone incompatible with dynamic roaming enemies | Changed to discrete perimeter openings, 1-3 per storey depth, any side, minimum distance rules from BEACON and STAIRCASE |
| 2026-03-17 | TRIGGER renamed to BEACON | Design change — storey completion is restoration-based not path-based | Renamed CellType.TRIGGER → CellType.BEACON throughout. Multi-beacon placement added. Dead end/corner placement for primary beacon. |
| 2026-03-16 | Staircase misaligned across storeys | Each storey generated its own staircase independently | Added forced_staircase: Vector2i param to generate() |
What Is Not Yet Implemented¶
| Feature | Notes |
|---|---|
| ~~Outpost building entity~~ | ✓ Done 2026-03-16 — OutpostBuilding.gd, 3 storeys stacked at Y=3.0 intervals |
| ~~Staircase chaining~~ | ✓ Done 2026-03-16 — forced_staircase param aligns staircase vertically |
| ~~Storey-to-storey transitions~~ | ✓ Done 2026-03-16 — preview mode (camera only) + travel mode (Genesis moves when on staircase) |
| ~~Genesis in outpost~~ | ✓ Done 2026-03-16 — OutpostGenesis.gd, WASD + click-to-move, flying, health bar |
| ~~Wall collision~~ | ✓ Done 2026-03-16 — WALL cells render as StaticBody3D with BoxShape3D |
| Building entrance cell type | Ground floor has no dedicated player entrance — ENTRY is enemy spawn only. Need CellType ENTRANCE or DOOR on ground floor boundary for planet-surface door. Deferred until planet map and outpost placement are designed. Genesis currently spawns at first entry_cell. |
| Enemy spawning | Drones spawn from ENTRY cells, roam toward power sources. Aliens roam toward Genesis (heat). Neither paths to BEACON — they roam dynamically. Next milestone. |
| BEACON restoration mechanic | Genesis walks to BEACON, spends materials, restores it. Each BEACON contributes independently to storey yield. Damaged BEACON increases bleed. |
| Reclaim Building | All BEACONs on all storeys restored → RECLAIM prompt in underground control room. Player optimises before triggering. Once triggered — building locked, trickle begins. |
| Tower placement | Player places towers on EMPTY cells — after enemies exist |
| Tower zone annotation | Tag open zones with recommended height coverage — after tower system |
| Underground factory entity | UNDERGROUND StoreyType generates correctly — building entity not yet built |
| Surface layer | Simpler than storeys — one chunk, seeded salvage nodes |
| Outpost entry from planet layer | Transition from planet map into outpost. Requires building entrance cell type above. |
| Remove debug print()s | Strip before any production use |
| ~~StoreyObjectSpawner (Beacon + Entry)~~ | ✓ Done 2026-03-21 — StoreyObjectSpawner.gd spawns BeaconObject and EntryPoint at special cells after render. StaircaseObject/ShaftConnector spawning still pending. |
File Locations¶
outpost/src/systems/generation/
StoreyLayout.gd — data contract (CellType + StoreyType enums, fields, helpers)
StoreyGenerator.gd — multi-pass generator (all parameters live here)
StoreyValidator.gd — A* pathfinder + BFS zone counter (read-only)
StoreyRenderer.gd — reads StoreyLayout, spawns 3D mesh nodes
WALL cells: StaticBody3D + BoxShape3D + MeshInstance3D
Floor cells: MeshInstance3D only (no collision)
outpost/src/systems/outpost/
OutpostBuilding.gd — owns N storeys, generates stack, tracks active floor
STOREY_HEIGHT = 3.0 units between floors
ascend()/descend() with preview vs travel mode
outpost/src/entities/genesis/
OutpostGenesis.gd — lightweight flying Genesis for outpost
WASD + click-to-move, hover bob, health system
Emits reached_staircase / left_staircase signals
collision_mask = 1 (collides with wall StaticBody3D nodes)
outpost/src/world/
OutpostGame.gd — wires building + genesis + UI
Preview mode: camera moves, Genesis stays
Travel mode: only active when on_staircase = true
outpost/src/ui/inspector/
SeedInspector.gd — debug UI: seed, sliders, StoreyType dropdown, stats, legend
outpost/scenes/
outpost_test.tscn — single storey debug workbench (not a game scene)
outpost_game.tscn — full game scene: building + genesis + HUD
Update this document whenever a parameter changes, a bug is fixed, or a new feature is implemented. The bug fix history is especially important — it captures why things are the way they are.