Star systems — a sector's celestial composition¶
Every sector renders as a small "solar system" in the cockpit windshield: a spectral-class-differentiated star (optionally binary), worlds on stable orbits, moons, rings, an optional asteroid belt, nebula haze, and an occasional collision debris ring. This doc is the canonical spec for that composition — the kinds of stars, the habitable zone, and how worlds are placed and how fast they orbit.
Status: ✅ Shipped — generated by
services/gameserver/src/services/celestial_service.py, persisted as aSectorCelestialrecord (per ADR-0073), served byGET /api/v1/sectors/{id}/system, and painted byservices/player-client/src/components/tactical/SolarSystemViewscreen.tsx. The numbers below are the as-built design; treat them as canon and tune in this doc.
This is the celestial contents of a sector. The sector's role and warp
topology (standard / nebula / asteroid / black-hole / hazard / anomaly types,
one-way warps, latent reveal) live in sectors.md; planet
gameplay (types, colonization, terraforming) lives in
../planets/.
Determinism & persistence¶
The composition is generate-once-then-stable, not re-rolled per request.
- All randomness flows from a SplitMix64 PRNG seeded by
sector_id × SECTOR_SEED_SALT(0x5EC7042102). Identical sector → identical composition. Per-body and per-station sub-seeds derive from that root seed — never from wall-clock,random, or unordered-collection iteration order. - The canonical composition (star kind(s), body slots + kinds + orbits,
belt, nebula, debris, the habitable zone) is persisted in a
SectorCelestialrow (1:1 with the sector), filled lazily on first visit and read-through thereafter. This is the stable answer to "what is in this sector." - Disposable flavor stays procedural and is NOT stored: starfield twinkle,
exact rubble offsets, and the live orbital-phase animation (worlds keep a
stored initial
phase_deg; their moment-to-moment angle is recomputed client- side each frame — see Orbits below).
Orbit radii below are a normalized 0.15–1.0 display space, not real AU — they exist to lay out a legible windshield, not to model astrophysics.
Star (sun) types¶
A sector's primary star is drawn by weight from ten spectral kinds. Dimmer classes are common; hot supergiants and degenerate remnants are rare.
| Kind | Label | Colour | Weight | Habitable zone (norm.) |
|---|---|---|---|---|
M_DWARF |
M-class Red Dwarf | #ff6b4a |
30 | 0.20 – 0.34 |
K_ORANGE |
K-class Orange Dwarf | #ffa94d |
20 | 0.28 – 0.45 |
G_YELLOW |
G-class Yellow Star | #ffe066 |
15 | 0.40 – 0.62 |
F_WHITE |
F-class White Star | #fff4d6 |
10 | 0.50 – 0.74 |
A_BLUE |
A-class Blue-White Star | #cfe5ff |
8 | 0.60 – 0.85 |
B_BLUE_GIANT |
B-class Blue Giant | #9ec5ff |
4 | 0.70 – 0.95 |
O_BLUE_SUPER |
O-class Blue Supergiant | #7fb0ff |
1 | 0.78 – 1.0 |
RED_GIANT |
Red Giant | #ff4d4d |
5 | 0.55 – 0.90 |
WHITE_DWARF |
White Dwarf | #e8f1ff |
4 | 0.16 – 0.26 |
NEUTRON |
Neutron Star | #b39dff |
2 | — (none) |
Plus BLACK_HOLE (#1a1026, label "Black Hole") which is selected by sector
type rather than the weight table (below). NEUTRON and BLACK_HOLE are
degenerate/dead and carry no habitable zone.
Binary stars. Outside the sector-type overrides, a star has a 12% chance
(BINARY_CHANCE) of a secondary companion drawn from the same weight table.
Hotter star → habitable zone pushed outward. The HZ band tightens close-in around dim stars (M dwarfs, white dwarfs) and moves outward around hot, luminous stars — see Habitable zone.
Sector-type overrides¶
The sector's own type (from sectors.md) can override the star
draw and add features:
| Sector type | Star | Extra |
|---|---|---|
BLACK_HOLE |
forced BLACK_HOLE (no binary, no HZ) |
accretion-disk + photon-ring render |
VOID |
no star (null) |
0–2 rogue bodies adrift in the dark |
STAR_CLUSTER |
primary star + 2–4 extra stars | multi-star field |
NEBULA |
normal star | nebula haze {hue 0–360, density 0.3–0.9} |
ASTEROID_FIELD |
normal star | asteroid belt always present |
| (all other types) | normal weighted star | asteroid belt at 15% (BELT_CHANCE_DEFAULT) |
Client rendering differentiates stars by kind: per-kind radius, white-dwarf / neutron glow treatment, and the black-hole accretion disk + photon ring.
Habitable zone¶
Each star kind defines a habitable-zone band (inner, outer) in the same
normalized 0.15–1.0 orbit space (table above). The viewport renders it as a soft
green annulus labelled "HABITABLE ZONE".
The HZ's size and distance are a function of the star's class (a proxy for its luminosity). Dim, low-luminosity stars hold a narrow zone close in; hotter, more luminous classes push it outward and widen it:
| Star class | HZ band | HZ width | Character |
|---|---|---|---|
| White Dwarf | 0.16 – 0.26 | 0.10 | narrowest, hugging the dim remnant |
| M-class Red Dwarf | 0.20 – 0.34 | 0.14 | tight, close in |
| K-class Orange Dwarf | 0.28 – 0.45 | 0.17 | — |
| G-class Yellow Star | 0.40 – 0.62 | 0.22 | Sol-like, mid-field |
| F-class White Star | 0.50 – 0.74 | 0.24 | — |
| A-class Blue-White | 0.60 – 0.85 | 0.25 | wide, far out |
| B-class Blue Giant | 0.70 – 0.95 | 0.25 | wide, far out |
| O-class Blue Supergiant | 0.78 – 1.0 | 0.22 | pushed to the outer edge |
| Red Giant | 0.55 – 0.90 | 0.35 | widest of all (a bloated, luminous shell) |
| Neutron Star / Black Hole | — | — | no habitable zone |
So the band is the star's, not the sector's: change the star class and the green annulus both moves (inner/outer distance) and resizes (width) accordingly. Degenerate/dead stars (neutron, black hole) carry none, so no habitable world can sit there.
A habitable world is guaranteed to sit inside the band. A planet counts as
habitable when its type is one of TERRAN, OCEANIC, TROPICAL, JUNGLE
(HABITABLE_KINDS) or its habitability_score ≥ 50 (HABITABLE_SCORE_MIN).
When a real Planet row is merged onto an orbit slot:
- It is first assigned a deterministic hash slot (from its UUID folded with the sector root seed).
- If the planet is habitable and that slot's orbit is outside the star's HZ, it is re-homed to the nearest free in-HZ slot (deterministic scan by distance to the HZ midpoint).
- If no free in-HZ slot exists (or the star has no HZ — neutron/black-hole/void), it falls back to the hash slot.
Non-habitable worlds ignore the HZ and keep their hash slot. The result: green worlds only ever appear in the green band, and a sector around a dead star never shows a habitable world in an impossible orbit.
The habitable planet-type subset is a view onto the canonical 12-value
PlanetType taxonomy in ../planets/ /
DATA_MODELS/planets.md — it is not a second
taxonomy. (Any UI showing Star-Trek-style class names like M_CLASS/L_CLASS
is a display artifact, not the stored type.)
Bodies & worlds¶
A sector holds 1–7 procedural filler bodies (0–2 for VOID), capped at
MAX_BODIES = 9, into which the sector's real Planet and Station rows are
merged. Procedural bodies are flavor — not click targets, not persisted as
planets — and use a weighted subset of PlanetType:
BARREN 30 · GAS_GIANT 25 · ICE 20 · DESERT 13 · VOLCANIC 12
Frost-line bias. Body kind is biased by orbital position f = slot / (n−1)
(0 = innermost, 1 = outermost), mirroring real protoplanetary sorting: gas-giant
weight scales by (0.3 + 1.4·f) (drift outward) and rocky weight by
(1.4 − 0.8·f) (rocky worlds inward). size_class: rocky 1–5 (weighted small),
gas giants 3–5. Rings: gas giants 40%, others 5%. Moons: 0–3, weighted up by
size_class and rendered coplanar (one shared tilted plane per planet).
Orbits and per-planet speed¶
Orbit distance is per-body and persisted. _make_orbits lays out a
cumulative gap walk: orbit[i] = orbit[i-1] + step · jitter with
step = (1.0 − 0.15) / count and jitter ∈ [0.7, 1.3], then rescales the whole
ladder so the outermost body sits at ≤ 1.0. Orbits are therefore monotonic and
well-spread (no two worlds share a ring by construction). Each body stores
orbit_au (4-dp) and an initial phase_deg (0–359). Stations get a random
orbit_au in 0.2–0.95.
Orbit speed is per-planet — inner worlds orbit faster (a Kepler-3rd-law
approximation), so a sector never shows every world locked in lockstep. The
client computes each body's angular velocity from its own orbit_au:
ω = ORBIT_SCALE · 2π / (180 + orbit_au · 420)
angle(t) = phase_deg·π/180 + t · ω
So ω ∝ 1 / (180 + 420·orbit_au): the innermost world (≈0.15) sweeps its circle
markedly faster than the outermost (≈1.0), while the +180 floor keeps even the
farthest orbit perceptibly moving. Pacing knobs (client):
| Constant | Value | Governs |
|---|---|---|
ORBIT_SCALE |
0.85 | planets + stations orbiting the star (perceptible) |
MOON_SCALE |
0.40 | moons around their planet (gentle) |
SHIP_SCALE |
0.60 | ship transit / drift between objects |
MOTION_SCALE |
= MOON_SCALE |
belt / debris churn (calm) |
The stored orbit_au + phase_deg are canonical; the moment-to-moment angle(t)
is recomputed each frame and never stored.
Planet rotation (axial spin)¶
Status: ✅ Shipped — generated into the
SectorCelestialcomposition bycelestial_service._make_bodyand rendered bySolarSystemViewscreen. Cosmetic (no gameplay effect yet); the day/night + seasons hook is future.
A planet's rotation (its spin on its own axis — its day/night cycle) is separate from its revolution (its orbit around the star). The orbit determines where a world is in its ring and how fast it travels that ring; rotation determines how fast the world itself turns. Two worlds sharing an orbit can still spin at very different rates.
Two per-body fields on the persisted composition (see
DATA_MODELS/galaxy.md → SectorCelestial),
generated deterministically from the body sub-seed:
rotation_period_hours— the length of one axial turn (the local "day"): gas giants ~6–14h (fast), large rocky/ice worlds (size ≥ 4) ~28–48h (slow), others ~12–30h; ~5% are tidally locked (~180–600h, an effectively frozen face).axial_tilt_deg— obliquity, 0–45°, which skews the spin axis.
Visualization: the windshield spins each planet's surface about its tilted axis
beneath the fixed, star-relative day/night terminator, so worlds visibly turn
on their own axis under sunlight — at per-planet rates (inner/gas-giant fast,
large worlds slow), never lockstep. Rate scales with 1/rotation_period_hours
via a calm SPIN_SCALE = 0.5 pacing knob (alongside ORBIT_SCALE/MOON_SCALE).
Like orbit phase, the live spin angle is recomputed per frame; only
rotation_period_hours + axial_tilt_deg are stored. (Pre-rotation persisted
skeletons fall back to a seed-derived value client-side, so old sectors spin too.)
Asteroid belt, nebula, debris¶
- Asteroid belt — always in
ASTEROID_FIELDsectors, 15% elsewhere; stored with inner/outer orbit bounds. (Resource ties: seequantum-resources.mdfor nebula fields.) - Nebula haze — present in
NEBULAsectors with{hue 0–360, density 0.3–0.9}; colours the field. - Debris ring — a 12% chance (
DEBRIS_CHANCE, only when a star and ≥1 body exist) of an ancient two-world collision remnant spread into an annulus around the orbital plane. Pure cosmetic flourish today; persisting it is what makes future destructible planets expressible (ADR-0073) — that mechanic is not yet built.
Planet discovery & naming¶
Status: ✅ Shipped — discovery columns and the discoverer-gated rename (
POST /api/v1/planets/{id}/name) onservices/gameserver/src/models/planet.py+services/gameserver/src/services/planet_naming_service.py; the 654-name corpus inservices/gameserver/src/data/planet_names.py. Schema: seeDATA_MODELS/planets.md.
Every world carries a discovery identity and a layered name, harmonized
with the per-player sector-discovery model (sectors.md) and the
formation-naming convention.
Discovery attribution. A planet records who first found it and when:
discovered_by (the discovering player; null = undiscovered / worldgen-seeded)
and discovered_at (timestamp). Discovery rides the existing per-player
discovery mechanic — a player discovers a planet by viewing its sector's
/system (which also marks per-sector feature discovery). It is not a new
mechanic; corp-shared discovery propagates through the same realtime-bus event as
warp and formation discovery. Planet discovery is topology-tier knowledge:
safe to share, and never leaked into ARIA's intelligence layer.
Two-layer naming. A planet's name resolves through three fields in precedence order:
custom_name → auto_name → name
auto_name— the generated default, assigned to every world. Theplanet_naming_servicedraws from a 654-name corpus and a prefix/suffix compound generator (so beyond the base corpus, worlds can carry compounded names). Genesis-formed worlds also receive anauto_name; procedural composer filler bodies render corpus names without persisting as planets.custom_name— the discoverer's chosen name (nullable). Layered overauto_nameso the generated original is never lost; clearing it falls back cleanly.name— the original worldgen string, retained as the final fallback.
Rename authority is the discoverer's. Only the player in discovered_by may
set a world's custom_name, via POST /api/v1/planets/{id}/name (gated on
discovered_by). Operators retain override authority. Names need not be globally
unique — two players may both name a world "Hope" — with custom names held to
length and profanity limits.
The /system response¶
GET /api/v1/sectors/{id}/system returns the merged composition:
star, optional extra_stars, nebula, belt, debris, the habitable-zone
band, and bodies[] (each with kind, orbit_au, phase_deg, size_class,
real, and for real planets planet_id, name, discovered_by,
habitability, owned). Viewing a sector's system marks planet discovery +
per-sector feature discovery (ADR-0073). The display name resolves
custom_name → auto_name → name, and only the discoverer may rename
(POST /api/v1/planets/{id}/name).
Open / future¶
- Destructible planets — persisting composition makes "a real world collapses into a debris ring" expressible; the mechanic itself is unbuilt (ADR-0073).
- Day/night & seasons — axial rotation (now shipped, above) is cosmetic; a
gameplay day-night or seasonal mechanic built on
rotation_period_hours/axial_tilt_degwould be a separate design. - Display-space orbits are deliberately not real AU; if a future feature needs physical distances (travel time, signal lag), that is a separate model.
Body kinds are intentionally decoupled from star class — placement biases by orbit position (the frost line) only, not by the host star's spectral type. There is deliberately no star-type → planet-type coupling.
Related¶
sectors.md— sector types & warp topologygeneration.md— worldgen pipeline & planet densityquantum-resources.md— nebula fields & resources../planets/colonization.md— planet types, habitability, productionDATA_MODELS/planets.md— canonicalPlanetTypeenum- ADR-0073 — celestial composition persistence + planet discovery/naming