Guides
Data model
Every object, field by field: what you must provide, what the library computes, and what stays undefined until you say otherwise.
Two layers of types
Every object in this library exists twice, and telling the two apart removes most of the confusion people have at the start.
…Input— what you write. Forgiving: ids, counts and empty collections can all be left out.- The plain type — what you get back. Complete: every id is filled in, every collection exists (possibly empty), every derivable number is computed.
import type { PlanetInput, Planet } from '@industrieh/starmap'
// What you write — no id, no index, no stations array.
const input: PlanetInput = { name: 'Vesper', orbit: 150, radius: 6 }
// What you get back — all three are there.
const planet: Planet = {
id: 'sol-p1', name: 'Vesper', index: 1, radius: 6,
orbit: 150, angle: 0, period: 0, stations: [],
}Reading the tables below
createGalaxy() — writing it yourself is allowed but rarely useful. Anything else is optional and stays undefined if you omit it; the library never invents a value that carries meaning.Ids
Ids must be unique across the whole galaxy — not per system, not per kind. Hyperlanes count too. That is what lets a single index hold everything, and what lets you store a bare string in a save file.
When you leave one out, it is built from the parent id and the array position:
star <systemId>-star // 'sol-star'
planet <systemId>-p<rank> // 'sol-p0', 'sol-p1'
station <planetId>-st<rank> // 'sol-p1-st0'
gate <systemId>-gate<rank> // 'sol-gate0'
field <systemId>-ast<rank> // 'sol-ast0'
lane lane-<from>-<to> // 'lane-sol-vega'Give explicit ids to things you refer to by name
sol-p1 is a position, not a name. Insert a planet before it and everything shifts — breaking every save that stored the old id. If your game refers to Earth, write id: 'earth'.System
The unit of place. Everything else lives inside one. In a system's local coordinates the star sits at the origin, so a planet's orbit is its distance from (0, 0); x and y on the system itself place it in the galaxy.
| Field | Type | Description |
|---|---|---|
idrequired | string | Unique across the whole galaxy. |
namerequired | string | Non-empty. |
x, yrequired | number | Position in the galaxy, in world units. |
starrequired | StarInput | Every system has exactly one. |
faction | { id, name } | Who holds the system. Free-form — the library imposes no vocabulary. |
security | number | 0 (lawless) to 1 (safe). Rejected outside that range. |
planets | PlanetInput[]default [] | In orbital order — the array position becomes planet.index. |
gates | GateInput[]default [] | Jump points. |
asteroidFields | AsteroidFieldInput[]default [] | Belts and clusters. |
localRoutes | LocalRouteInput[]default [] | Links between two objects of this system. |
outerLimitderived | number | The system's edge. Defaults to how far its own contents reach — with no margin. Set it yourself if you want empty space past the outermost object. |
Star
| Field | Type | Description |
|---|---|---|
radiusrequired | number | In world units. Must be strictly positive. |
id | stringdefault <systemId>-star | Rarely worth setting. |
name | stringdefault the system name | Sol the system, Sol the star. |
class | StarClass | Spectral class: O B A F G K M, or any other string. |
Planet
| Field | Type | Description |
|---|---|---|
namerequired | string | Non-empty. |
orbitrequired | number | Distance from the centre of the star. Strictly positive. |
radiusrequired | number | The planet itself. Strictly positive. |
angle | numberdefault 0 | Where it sits at t = 0, in degrees. |
period | numberdefault 0 | Seconds for a full orbit. 0 means it never moves. |
indexderived | number | Its rank in the array. Validation rejects a mismatch. |
type | PlanetType | terran, gas, frozen… or any other string. |
moons | number | A count. The library never models moons as objects. |
habitable | boolean | Yours to interpret. |
resources | string | Free-form. Never read. |
stations | StationInput[]default [] | Orbiting it. |
Station
A station orbits a planet, not the star — so its orbitis measured from the planet's centre, and it is carried along as the planet travels.
| Field | Type | Description |
|---|---|---|
namerequired | string | Non-empty. |
orbitrequired | number | Distance from the centre of its planet. Strictly positive. |
angle | numberdefault 0 | Degrees, at t = 0. |
period | numberdefault 0 | Seconds per orbit around its planet. |
planetIdderived | string | Always filled in. The only back-pointer in the model. |
type | StationType | trade, mining, shipyard… or any other string. |
docks | number | Free-form. Never read. |
Gate
A gate is an object inside a system — something a ship flies to. The hyperlane is the galaxy-level edge that pathfinding walks. Modelling both lets a system have three gates on one lane, or a lane with no gate at all.
| Field | Type | Description |
|---|---|---|
namerequired | string | Non-empty. |
orbitrequired | number | Distance from the star. Strictly positive. |
angle | numberdefault 0 | Degrees. Gates never move. |
links | string[]default [] | System ids this gate reaches. Every one must exist, and none may be its own system. An empty list is a dead gate — allowed on purpose. |
status | GateStatusdefault 'nominal' | nominal or unstable, or any other string. |
Asteroid field
Discriminated by kind. A belt is an arc of a ring around the star; a cluster is a pocket anywhere in the system.
// A belt: an arc between two orbits.
{ kind: 'belt', name: 'Inner Belt', orbit: 220, width: 30, arcStart: 0, arcSweep: 360 }
// A cluster: a pocket at a fixed spot.
{ kind: 'cluster', name: 'The Shoals', x: -180, y: 90, radius: 40 }| Field | Type | Description |
|---|---|---|
namerequired | string | Non-empty. |
kindrequired | 'belt' | 'cluster' | Decides which of the two shapes below applies. |
orbit, widthrequired | number | Belt only. Ring radius and radial thickness. |
arcStart, arcSweeprequired | number | Belt only, in degrees. arcSweep: 360 is a full ring; it is rejected past 360. |
x, y, radiusrequired | number | Cluster only. Centre and reach, in system coordinates. |
rocks | [x, y, r][]default [] | Pre-placed asteroids. The library never generates any. A tuple rather than an object, because a field can hold hundreds and {"x":1,"y":2,"r":3} is four times the JSON of [1,2,3]. |
density | number | 0 to 1. A hint for your own logic — yield, danger. Never read. |
richness | Richness | Free-form label. |
Galaxy
The root. Three keys, and only the first is required of you.
| Field | Type | Description |
|---|---|---|
systemsrequired | SystemInput[] | At least the array. |
lanes | LaneInput[]default [] | The inter-system network. You decide what connects to what. |
seed | string | Free-form, copied to meta.seed. Put your generator's seed here so you can rebuild this universe later. |
generatedAt | string | Free-form timestamp, copied to meta. Never read. |
Hyperlane
Undirected: from and to are interchangeable, and declaring the same pair twice is an error. A lane from a system to itself is rejected.
| Field | Type | Description |
|---|---|---|
from, torequired | string | System ids. Both must exist. |
lengthderived | number | Straight-line distance between the two systems. Override it to make a short lane cost more — findRoute() weighs by this number, not by geometry. |
id | stringdefault lane-<from>-<to> |
length is a cost, not just a distance
length and nothing else, it doubles as a travel-cost knob. A dangerous shortcut can be given a large length so routing avoids it unless it is the only way through.Meta
Counters and a couple of labels, all computed. Validation re-derives them and rejects a galaxy whose meta disagrees with its contents — which is how a hand-edited save file gets caught.
galaxy.meta
// {
// seed: 'quickstart',
// formatVersion: 2,
// systemCount: 2, laneCount: 1,
// planetCount: 2, stationCount: 1,
// gateCount: 1, asteroidFieldCount: 0,
// galaxyRadius: 449.44, // the disc enclosing every system
// }formatVersion is your save-compatibility check
FORMAT_VERSION when loading a save and you will know the contract moved on before you hit a missing field.Local route
A link between two objects inside the same system — a docking approach, a mining run, a shortcut. Both ends are { kind, id } references, and validation checks three things: the id exists, the kind matches, and both ends belong to this system.
localRoutes: [
{
from: { kind: 'planet', id: 'sol-p1' },
to: { kind: 'station', id: 'sol-p1-st0' },
type: 'docking',
},
]Open unions
PlanetType, StarClass, StationType, FactionId, Richness, GateStatus and RouteType are open: the listed values appear in autocomplete, and any other string is still accepted.
type PlanetType = 'lava' | 'rocky' | 'terran' | /* … */ | (string & {})
{ type: 'terran' } // suggested
{ type: 'shattered' } // yours, and just as validThe (string & {}) is what stops TypeScript from collapsing the union down to plain string and losing the suggestions. The library never reads these values — they exist for you to map to appearance or behaviour.
What is deliberately missing
Things you might expect, and the reason they are not here:
- Colours, labels, sprites. Rendering is not this library's job. Map
planet.typeto an appearance in your own code. - Moons as objects. Only a
moonscount. Modelling them would double the depth of the tree for something most games draw as a detail. - Elliptical orbits. Circles only. Ellipses need a focus, an eccentricity and an argument of periapsis to mean anything, and almost no game needs them.
- Game state. No ownership, no stock, no visited flag. That is the frozen-terrain rule, and it is the one decision the whole library leans on.
If a field would mean different things in two different games, it does not belong in the model.