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_levelis 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 onPlanet.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-captable) 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)
-
C_slots(hard):max L: usable_slots ≥ CITADEL_MIN_CELLS[L]— verified againststructures.py. TERRACED 1.0× reproduces the current size→level cap exactly by construction (same function, same slot count at mult = 1.0). -
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_footprintis a validated property of each template, asserted by the build-time harness, not assumed from the class label. -
C_practical(honest achievable floor, resolves Q5):max L: usable_slots ≥ CITADEL_MIN_CELLS[L] + H[L], withH[L]the spare cells a standard build needs to clearFLOOR_AREA[L]/HOUSING[L]at achievable building levels. ProvisionalH = {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, butMIN_CELLS[3]+H[3]=7>6would give L2).
📐 Design-only. The headline at-settle display is
C_practical(the guaranteed achievable floor);C_hardappears 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 bystructures.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_hoursbuild 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 theH[L]spare cells to clearFLOOR_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_LEVELSshould expand once the level-6+ table is finalized.✅ Shipped — level naming.
citadel_service.CITADEL_LEVELSships 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_ratesapplies(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).
- Meet population requirement — current pop must hit max for current level.
- Stockpile resources — credits + commodities on the planet (or in the safe).
- 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").
- Initiate upgrade — credits and resources consumed immediately, construction timer starts.
- Wait — citadel remains functional during build; population cap stays at current level until done; partial defensive bonus (+50% of next-level effects) during construction.
- Complete —
check_upgrade_completionapplies 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 byGET /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_buildingappends a{type, started_at, complete_at}entry to adefense_build_queueinplanet.active_events(no migration), andget_available_buildingslazily settles finished builds on read (self-committing on GET) — finished entries move into the operationaldefense_buildingscounts. 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:
- The in-progress upgrade is cancelled in the same transaction as the prerequisite-loss state change.
- 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).
- Realtime event
citadel.upgrade_cancelledpublishes onpersonal:{owner_user_id}:{ "planet_id": "<uuid>", "cancelled_upgrade": "<target_level>", "reason": "prerequisite_building_offline", "lost_building": "<building_name>", "credits_refunded": <int>, "at": "<iso8601>" } - 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_levelinteger scale (L3 → 2+, L4 → 5+, L5 → 8+). Target per ADR-0059 N-D4: replace the flat-level check with the specific-building scan againstPlanet.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 upgrade —
POST /planets/{id}/citadel/cancelrefunds 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_cancelledevent (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 standaloneplanetary/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 flatdefense_levelscalar), automatic prerequisite-loss cancel +citadel.upgrade_cancelledevent (📐 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 fromDEFENSE_BUILDINGS).Drift to reconcile (code-wins):
defense.mdlists the Orbital Defense Platform cost as 500,000 cr + 50,000 ore + 25,000 equipment, butDEFENSE_BUILDINGScharges 500,000 cr only. The planet-type enum isTERRANin code (models/planet.py) while several docs sayTERRA.