Skip to content

Citadels

The fortified structure that is a colony — population, storage, defenses, and a secure vault. Citadel level drives the colony's phase, capacity, and what defensive buildings unlock.

Citadel level is grid-derived

✅ Shipped (CRT-1/2/3). The citadel level is derived authoritatively from the buildings placed on the planet's structures grid — the grid is the source of truth, and citadel_level is a cached scalar kept in sync with it. citadel_service.derive_citadel_level(planet.structures) is the faithful inverse of the level ladder; the upgrade path runs place → derive → cache: an upgrade places the required grid buildings, re-derives the level from the grid, and caches the derived value on Planet.citadel_level (citadel_service.py:586-647). Grid player-management endpoints and a SpaceDock-style grid UI let owners arrange the lattice.

The colony grid is landscape-driven

📐 Design-only. Gated on ADR-0091. Prescriptive spec: ../../SYSTEMS/citadel-grid.md.

A planet has one playable grid per colony, materialized at settle from the at-launch expedition result's landscape shape, not from planet size alone. The shape is one of five natural classes plus one engineered class; each is a multiplier on the planet-size slot envelope.

Shape class Mult on B Topology Max footprint Trade
COMPACT 0.85× dense near-square 2×2+ smallest, most efficient upkeep, lowest ceiling
TERRACED 1.00× stacked tiers 2×2+ the neutral baseline shape (the anchor)
LINEAR 1.10× 1–2-wide strip 2×1 only (no 2×2) edge bonuses, cannot host large footprints; caps citadel at L4 regardless of slot count
IRREGULAR 1.15× disjoint clusters 2×2+ (gappy) mixed terrain, fragmented, clearing cost
SPRAWLING 1.30× large loose footprint 2×2+ highest ceiling but highest per-slot upkeep + baseline power draw (the sprawl tax)
ENGINEERED 1.00× near-square 2×2+ ARTIFICIAL + the always-valid safe default/fallback

📐 Design-only [P]. SPRAWLING carries a live sprawl tax even without combat: per-slot upkeep and baseline power draw scale with footprint size, so the biggest grid is not a free maximum. Capacity is measured as usable contiguous footprint, not raw slot count: LINEAR's 2×1 cap means a high-count strip is not strictly better than a packable COMPACT.

Slot formula

📐 Design-only [P].

B            = 4 + 2·size                              # planet-size envelope, ∈ [6, 24]
usable_slots = clamp(round(mult · B), 6, 32)           # round-half-up; cap raised 30 → 32
size B COMPACT 0.85 TERRACED 1.00 LINEAR 1.10 IRREGULAR 1.15 SPRAWLING 1.30
1 6 6 6 7 7 8
5 14 12 14 15 16 18
10 24 20 24 26 28 31

A size-5 planet yields 12 (compact) to 18 (sprawling) slots by site — a ~50% swing in ceiling. That swing is why discovery exists.


Capacity cap (landscape-driven)

📐 Design-only. This section supersedes the size→max-level cap (the DECISIONS.md citadel-size-cap table) on acceptance of ADR-0091. Usable slots — not size — caps the maximum citadel level, because footprints are multi-cell and shape determines how much the grid can physically hold.

The cap uses a three-channel model (the authoritative resolution of the H[L] arithmetic correction):

C_site    = min( C_slots , C_footprint , C_practical )
C_hard    = min( C_slots , C_footprint )               # permanent, site-bound (R3)
  1. C_slots (hard): max L: usable_slots ≥ CITADEL_MIN_CELLS[L] — verified against structures.py. TERRACED 1.0× reproduces the current size→level cap exactly by construction (same function, same slot count at mult = 1.0).

  2. C_footprint (hard, NEW — code-grounded): topology gates the ceiling independently of slot count. Footprint ladder (verified against shipped key buildings): L1–L3 need 1×1; L4 needs a 2×1 (SPACEPORT); L5 needs a 2×2 (ADMIN_SPIRE). Therefore LINEAR (2×1 max, no 2×2) structurally tops out at L4 despite its higher slot multiplier — the canonical "more slots ≠ higher ceiling" case. C_footprint is a validated property of each template, asserted by the build-time harness, not assumed from the class label.

  3. C_practical (honest achievable floor, resolves Q5): max L: usable_slots ≥ CITADEL_MIN_CELLS[L] + H[L], with H[L] the spare cells a standard build needs to clear FLOOR_AREA[L]/HOUSING[L] at achievable building levels. Provisional H = {1:0,2:1,3:2,4:3,5:4} [P], calibrated by the build-time harness — not asserted. H[L] is a cell-count input to the inverse function, never an addend to a level. Note: H[L] lives in the honest floor (C_practical), not in the hard cap — adding H[L] to the hard-cap threshold would lower the current size→level cap on small planets (verified: size-1 6-plot = L3, but MIN_CELLS[3]+H[3]=7>6 would give L2).

📐 Design-only. The headline at-settle display is C_practical (the guaranteed achievable floor); C_hard appears only as a clearly-labeled "potential ceiling — requires densification investment" secondary, never the advertised number (M5). This closes the "advertised L5-capable, actually capped lower, discovered only after settle" gotcha.

Population stays a separate gate. HOUSING is a population requirement satisfied by the existing population→phase ladder (see colonization.md, untouched), not by slots. Site choice sets the spatial ceiling; population gates independently exactly as today.

T1 excavation (M7): players can raise a site's starting ceiling upward with effort (T1 ships in v1). R3 is reframed: site sets the starting ceiling + the cost-curve to raise it, not an immovable cap. T2 orbital annex: design-only/v1-optional. T3 landscape re-template: ⚑ DEFERRED (destructive migration of a populated colony's planet.structures).


Hidden grid & progressive reveal

📐 Design-only [P]. The full grid is seeded server-side at settle by structures.py (single writer), but cells are fogged (visibility, not absence). The grid is hidden until the citadel reveals it tier by tier.

  • The L1 founding tranche reveals instantly at settle.
  • Each subsequent tranche unfogs atomically when the L(N-1)→LN upgrade completes — reusing the existing upgrade_hours build timer (see citadels.md upgrade workflow). No new timer, and no fog↔timer rollback edge case: a cancelled/failed upgrade leaves no fog in a half-state.
  • Reveal schedule [P] (cumulative fraction unfogged by citadel level): L1 35% · L2 55% · L3 70% · L4 85% · L5 100%.
  • Soft-lock guard: revealed(L) is always ≥ the cells level L genuinely needs to build — its key-building footprint plus the H[L] spare cells to clear FLOOR_AREA[L]/HOUSING[L] at achievable building levels. Monotonic.
  • Frontier reveal (M14): radiate outward from the citadel core along the connectivity graph; severed clusters stay dark until reached or bridged. Player-directed reveal is DEFERRED.
  • Reclaimable / CLEARABLE cells are shown separately from the headline usable-slot count (M16).

The grid GET returns revealed cells fully and fogged cells as silhouette-only (position + "unrevealed"), so the Grid Manager renders the shape outline with locked cells.


Floor guarantee — validated template library

📐 Design-only. No site is a trap. Grids are generated from a constrained shape-template library: each (shape class, size-bucket) maps to a small set of pre-validated silhouette templates whose packing and per-level reveal tranches are validated offline, once, to guarantee the key buildings + the FLOOR_AREA/HOUSING economy fit at the shape's allowed max level.

The template library is the pool from which the at-launch expedition roll draws its result — the planet's profile (type, size, hazard, terrain) weights which templates are more likely to be drawn. This is a generation-time draw from a validated pool, not a pre-stored per-planet map. Traits overlay terrain/hazard/deposit onto cells without altering the validated topology. There is no runtime bin-packing solver — the floor guarantee is a build-time test over a finite library. ENGINEERED is the always-valid fallback. A bad roll is bland, never bricked.


Already-colonized planets

📐 Design-only. Already-colonized planets keep their existing size-derived near-square grid and their existing cap — untouched. Only newly-settled sites use the landscape generator. ENGINEERED is the reconciliation home for the current near-square shape.


Levels and capacities

Authoritative source: services/gameserver/src/services/citadel_service.py:CITADEL_LEVELS.

Level Name Max population Safe storage (cr) Drone capacity Upgrade cost (cr) Upgrade hours Resources required
0 No Citadel 0 0 0
1 Outpost 1,000 100,000 10 0 (free establish) 0
2 Settlement 5,000 500,000 25 50,000 48 500 fuel_ore + 200 equipment
3 Colony 15,000 2,000,000 50 150,000 72 1,500 fuel_ore + 500 organics + 800 equipment
4 Major Colony 50,000 10,000,000 100 500,000 120 5,000 fuel_ore + 2,000 organics + 3,000 equipment
5 Planetary Capital 200,000 50,000,000 200 2,000,000 240 15,000 fuel_ore + 8,000 organics + 10,000 equipment

Per-level upgrade costs, durations, and resource requirements are defined in the CITADEL_LEVELS constant in citadel_service.py. The constant is the source of truth — this table mirrors it for reference.

📐 Design-only — Levels 6–10. The full citadel progression runs to level 10. Levels 6–10 (continuing the lifecycle progression toward larger Capital and post-Capital phases) are design-pending: numeric costs, capacities, and names need a balance pass. Code currently ships levels 1–5; the cap in citadel_service.CITADEL_LEVELS should expand once the level-6+ table is finalized.

✅ Shipped — level naming. citadel_service.CITADEL_LEVELS ships the lifecycle progression above (Outpost / Settlement / Colony / Major Colony / Planetary Capital), matching the target spec.

Per-level passive bonuses

Level Production bonus Notes
1 +5% all production Establishes basic colony multiplier.
2 +10%
3 +15%
4 +20%
5 +25% Capped here; matches the cap in ../../SYSTEMS/planetary-production-tick.md.

✅ Shipped (2026-06-14) — citadel production bonus. planetary_service._calculate_production_rates applies (1 + 0.05 × citadel_level) to fuel/organics/equipment output. Proven live: a planet's output at L0 → L1 → L5 scaled 1000 → 1050 → 1250 (+5%/level). See FINDINGS.md.

Defensive buildings unlocked by citadel level

citadel_service.DEFENSE_BUILDINGS:

Building Min citadel Max count by level Cost (cr) Build hours Effect
Orbital Defense Platform 4 L4: 1, L5: 3 500,000 168 2-sector range, 500 dmg/round
Long-Range Scanner Array 2 L2: 1, L3: 1, L4: 2, L5: 2 75,000 48 2-sector detection
Automated Turret Network 3 L3: 2, L4: 4, L5: 6 150,000 72 3 anti-drone kills/round

Plus shields, drones, rail guns, and the defense grid documented in defense.md.

Upgrade workflow

An upgrade is gated by population, resources, and defensive prerequisites, runs over a construction timer, and on completion places the tier's grid buildings and re-derives the new citadel_level from the grid (the cached value is the grid-derived level, clamped by the size cap).

  1. Meet population requirement — current pop must hit max for current level.
  2. Stockpile resources — credits + commodities on the planet (or in the safe).
  3. Meet defense prerequisite — specific defensive buildings must be present and operational, enforced in citadel_service.start_upgrade:
Citadel level Required defensive buildings (all must be operational)
L1 Outpost None (issued at colonization)
L2 Settlement None (auto-progression by population)
L3 Colony Defense Grid L1 OR Automated Turret Network
L4 Major Colony Defense Grid L2 AND Automated Turret Network AND Shield Generator L4
L5 Planetary Capital Defense Grid L2 AND Orbital Defense Platform AND Rail Gun Battery AND Shield Generator L8

The check scans Planet.defense_buildings; missing/offline buildings fail the upgrade with prerequisite_building_missing or prerequisite_building_offline. The player-client surfaces the specific missing building ("Upgrade blocked — requires Shield Generator L4 operational").

  1. Initiate upgrade — credits and resources consumed immediately, construction timer starts.
  2. Wait — citadel remains functional during build; population cap stays at current level until done; partial defensive bonus (+50% of next-level effects) during construction.
  3. Completecheck_upgrade_completion applies the new level when the timer elapses. The same prerequisite check runs again; if a required building was destroyed mid-upgrade, the timer cancels and the player is notified.

Build time & the construction indicator

Citadel upgrades are time-based (lazy advance-on-read via check_upgrade_completion, the same pattern as terraforming/market regen — no scheduler). Per-level durations are canon in citadel_service.CITADEL_LEVELS:

Upgrade Duration
L0 → L1 Outpost instant — the founding act is the construction (issued at colonization)
L1 → L2 Settlement 48 h (2 days)
L2 → L3 Colony 72 h (3 days)
L3 → L4 Major Colony 120 h (5 days)
L4 → L5 Planetary Capital 240 h (10 days)

✅ Shipped — live construction indicator (2026-06-14). While an upgrade is in progress the landed-console "Citadel Defense Systems" panel renders a live construction readout: an amber marching-stripe progress bar, a per-second countdown, an absolute ETA, and the target level ("→ Settlement"). It ticks in real time off citadel_upgrade_started_at/citadel_upgrade_complete_at (both already returned by GET /planets/{id}/citadel) and auto-reveals the finished level when the timer elapses. A compact build beacon (🏗️ NN% · Nd Nh) also shows in the minimized-surface strip so progress is visible when the surface vista is collapsed. The server timer was already in place; this surfaces it in the primary surface with real-time precision. See FINDINGS.md.

✅ Shipped — time-based defense-building construction (2026-06-14). Defense buildings now construct over time instead of instantly, using the canon build_hours (Long-Range Scanner Array 48 h, Automated Turret Network 72 h, Orbital Defense Platform 168 h). The mechanic mirrors the citadel upgrade: build_defense_building appends a {type, started_at, complete_at} entry to a defense_build_queue in planet.active_events (no migration), and get_available_buildings lazily settles finished builds on read (self-committing on GET) — finished entries move into the operational defense_buildings counts. Capacity counts operational + in-progress, so a pending build reserves its slot. The landed console renders a per-building countdown + marching progress bar and a +N🏗️ badge; the ops clock ticks while any build is in progress. Proven live: scanner queued (credits −75 k, operational count unchanged, complete_at +48 h), DB held the queue entry, lazy-settle on read completed it to {scanner_array: 1}. See FINDINGS.md.

📐 Design-only — partial defensive bonus during construction. Step 5's "+50% of next-level effects during construction" is specified but not yet applied in code (the planet operates at the current level's stats until the timer completes).

Mid-upgrade cancellation flow (per ADR-0059 N-F3 / N-V5). When a prerequisite-loss event fires during an active upgrade:

  1. The in-progress upgrade is cancelled in the same transaction as the prerequisite-loss state change.
  2. All credits + resources committed to the upgrade refund to the player's wallet (full refund, not 50% — the loss event was external, not a player-initiated cancel).
  3. Realtime event citadel.upgrade_cancelled publishes on personal:{owner_user_id}:
    {
      "planet_id": "<uuid>",
      "cancelled_upgrade": "<target_level>",
      "reason": "prerequisite_building_offline",
      "lost_building": "<building_name>",
      "credits_refunded": <int>,
      "at": "<iso8601>"
    }
    
  4. ARIA narrates: "Your <target_level> upgrade was cancelled — <lost_building> is offline. Rebuild it to resume the upgrade."

The existing citadel level stays when a prerequisite goes offline — passive defense bonuses persist. Only future-progression friction is felt; rebuilding the prerequisite restores upgrade access without forcing a re-acquisition cycle.

Player-initiated cancellation follows the 50%-refund schedule (the loss is voluntary; the partial refund covers irreversible setup work).

🐛 Bug — prerequisite framing. Design intent (above) maps the prerequisite to specific structures. The shipped check uses a flat Planet.defense_level integer scale (L3 → 2+, L4 → 5+, L5 → 8+). Target per ADR-0059 N-D4: replace the flat-level check with the specific-building scan against Planet.defense_buildings. Planets with incomplete prerequisite buildings get a one-time check at deploy and downgrade any over-leveled state with an explanatory ARIA narration.

The safe vault

Every citadel has a secure storage area that survives even if the citadel is destroyed.

Citadel level Safe capacity (cr-equivalent)
1 100,000
2 500,000
3 2,000,000
4 10,000,000
5 50,000,000

Safe contents: - Credits (direct). - Any commodity that fits in cargo (ore, organics, equipment, etc.). - Special resources (Quantum Shards, Lumen Crystals, Prismatic Ore). - Genesis Devices. - Ship upgrades / equipment.

Properties: - ✅ NOT lost on citadel destruction. - ✅ Attackers cannot steal from the safe. - ✅ Transfers with planet ownership. - ✅ Only the planet owner can deposit/withdraw.

Usage: - Deposit/withdraw when docked at the planet. - Optional auto-transfer of planetary production into the safe. - Emergency evacuation: pull all safe contents to ship cargo (subject to cargo capacity).

Citadel destruction consequences

When citadel hull hits 0 in combat: - Population dies. - Production facilities destroyed. - Planet reverts to uncolonized — anyone can re-colonize it. - All defensive structures (drones, shields, platforms, rail guns) lost. - Safe contents preserved — recoverable by the previous owner.

Strategic patterns

  • Multiple Level 2–3 citadels > single Level 5 for most players: distributed risk, distributed production, multiple safe storage locations.
  • Level 5 is for prestige and fortresses — 50M-credit safe, near-impregnable defense, team headquarters potential.
  • Resource pre-stockpiling is critical — long upgrade timers mean the resources sit unused for days during construction.

Player-facing affordances

  • Citadel panel shows current level, next level requirements, progress bars on each requirement.
  • Safe vault UI with deposit/withdraw, auto-deposit toggle, content list. (✅ credit deposit/withdraw shipped; ✅ commodity storage shipped 2026-06-14 — fuel ore / organics / equipment move planet-stockpile ↔ safe, valued at base trade prices against the single cr-equivalent cap per ADR-0082, with a Stored Goods content list in the safe UI; auto-deposit toggle + special-resource/Genesis/upgrade storage remain 📐 design-only.)
  • Live construction indicator — real-time progress bar + countdown + ETA in the landed console, plus a build beacon in the minimized-surface strip (see "Build time & the construction indicator").
  • Cancel an in-progress upgradePOST /planets/{id}/citadel/cancel refunds 50% of the credits paid (resources are not returned) via a two-step Cancel button in the construction indicator (2026-06-14). 📐 The automatic prerequisite-loss cancellation + citadel.upgrade_cancelled event (ADR-0059 N-F3) is still design-only.
  • Defense building queue — defense buildings construct over time (canon build_hours: scanner 48 h, turret 72 h, orbital platform 168 h) via a JSONB construction queue lazy-settled advance-on-read; landed console shows a per-building countdown + progress bar; a pending build reserves its capacity slot (2026-06-14). See "Build time & the construction indicator".
  • Upgrade pre-flight checklist — the citadel-upgrade confirm lists the credit + population gates with ✓/✗ so the player sees blockers before committing (2026-06-14).
  • Safe quick-fill presets — the safe deposit/withdraw form offers 25/50/75% presets beside Max (2026-06-14).

Source map

Topic Path
Citadel service & level table services/gameserver/src/services/citadel_service.py
Planet citadel fields services/gameserver/src/models/planet.py
Defense building constants services/gameserver/src/services/citadel_service.py:DEFENSE_BUILDINGS
Combat consequences services/gameserver/src/services/combat_service.py:_resolve_planet_combat
Genesis-derived starting citadel services/gameserver/src/services/genesis_service.py

Status: 🚧 Partial — Planet.citadel_* columns, citadel_service.py (level table, time-based upgrade workflow with lazy completion, safe credit deposit/withdraw), the standalone planetary/CitadelManager.tsx, and the live construction indicator in the landed console (pages/GameDashboard.tsx, 2026-06-14) are in place. Shipped 2026-06-14 (NEON citadel pass): ✅ +5%/level production bonus, ✅ population gate before upgrade, ✅ upgrade cancel + 50% refund, ✅ time-based defense-building construction (queue + lazy settle), ✅ upgrade pre-flight checklist, ✅ safe quick-fill presets. Outstanding gaps: specific-building prerequisite scan (🐛 ADR-0059 N-D4 — code still uses the flat defense_level scalar), automatic prerequisite-loss cancel + citadel.upgrade_cancelled event (📐 ADR-0059 N-F3), ✅ shield-generator construction timer (time-based, level × 6 h, ADR-0086, shipped + proven live 2026-06-16: credits charged up front, level/strength advance on completion via lazy settle, concurrent upgrade rejected), +50% partial bonus during build (📐), ✅ safe commodity storage (cr-equivalent cap, ADR-0082, 2026-06-14), ADR-0076 per-unit defense pricing (📐), and Rail Gun Battery + Defense Grid buildings (📐 — absent from DEFENSE_BUILDINGS).

Drift to reconcile (code-wins): defense.md lists the Orbital Defense Platform cost as 500,000 cr + 50,000 ore + 25,000 equipment, but DEFENSE_BUILDINGS charges 500,000 cr only. The planet-type enum is TERRAN in code (models/planet.py) while several docs say TERRA.