starmap

Reference

API

Every export of @industrieh/starmap, with its signature. Everything comes from the package root — there are no sub-paths.

Looking for explanations rather than signatures?

The guides cover the same ground with reasons attached: Querying, Animating, Randomness.

Building

createGalaxy

function
The entry point. Takes your description, fills in what can be computed, checks the result, and returns a complete Galaxy.
function createGalaxy(
  input: GalaxyInput,
  options?: CreateGalaxyOptions,
): Galaxy

Parameters

inputGalaxyInput
At minimum { systems: [...] }. Not mutated.
options.validateboolean = true
Set to false to skip the check. Only on a hot path whose input you have already validated.

Returns

A Galaxy: meta, systems, lanes. Treat it as immutable.

Throws

GalaxyValidationError when the finished galaxy is inconsistent — error.issues holds every problem, not just the first. TypeError if input is not an object with a systems array.
const galaxy = createGalaxy({
  systems: [
    { id: 'sol', name: 'Sol', x: 0, y: 0, star: { radius: 18 } },
    { id: 'vega', name: 'Vega', x: 420, y: 160, star: { radius: 22 } },
  ],
  lanes: [{ from: 'sol', to: 'vega' }],
})

galaxy.systems[0].star.id   // 'sol-star'  — derived
galaxy.lanes[0].length      // 449.44      — derived from the positions

FORMAT_VERSION

constant
The version of the data contract. Copied into meta.formatVersion at build time, and bumped on every breaking change to the shape.
const FORMAT_VERSION: number   // currently 2
if (save.meta.formatVersion !== FORMAT_VERSION) {
  console.warn('This save predates the current data contract')
}

Checking

validateGalaxy

function
Check a galaxy and return its problems. Never throws, whatever you pass it — which is what makes it the right tool for data read back from disk.
function validateGalaxy(galaxy: Galaxy): GalaxyIssue[]

Parameters

galaxyGalaxy
Anything, really. A malformed value is reported, not thrown.

Returns

An array of { path, message }, empty when the galaxy is sound. path locates the value, e.g. systems[3].planets[0].orbit.
const galaxy = JSON.parse(json) as Galaxy
const issues = validateGalaxy(galaxy)

if (issues.length) {
  console.error(issues.map((i) => `${i.path}: ${i.message}`).join('\n'))
}

assertValidGalaxy

function
The throwing form of validateGalaxy(). This is what createGalaxy() calls internally.
function assertValidGalaxy(galaxy: Galaxy): void

Throws

GalaxyValidationError if there is at least one issue.

GalaxyValidationError

class
Thrown by createGalaxy() and assertValidGalaxy(). The message lists every problem; issues gives you the same list structured.
class GalaxyValidationError extends Error {
  readonly issues: readonly GalaxyIssue[]
}
try {
  createGalaxy(input)
} catch (error) {
  if (error instanceof GalaxyValidationError) {
    for (const issue of error.issues) {
      console.error(issue.path, issue.message)
    }
  }
}

Querying

indexGalaxy

function
Build an id → object lookup covering every kind: systems, stars, planets, stations, gates and asteroid fields.
function indexGalaxy(galaxy: Galaxy): GalaxyIndex

Returns

A ReadonlyMap of IndexEntry. Each entry carries its kind, the obj itself, and the systemId it belongs to.

Build it once

Every call is a full walk. A galaxy never changes after it is built, so the index never goes stale — keep it for the session.
const index = indexGalaxy(galaxy)   // walks the whole universe — do this once

const entry = index.get('sol-p1-st0')
if (entry?.kind === 'station') entry.obj.docks   // typed as Station

indexSystems

function
Build an id → system lookup. Cheaper than the full index when systems are all you need.
function indexSystems(galaxy: Galaxy): SystemsById

Returns

A ReadonlyMap<string, StarSystem>.

systemOf

function
The system an object belongs to, from its id alone — for any kind of object. A system id returns that same system, which makes it safe to call on an id you have not inspected.
function systemOf(
  source: Galaxy | GalaxyIndex,
  id: string,
): StarSystem | null

Parameters

sourceGalaxy | GalaxyIndex
Passing a Galaxy re-indexes the universe on every call. In a loop, pass the index.
idstring
Any object id.

Returns

The owning system, or null if no object carries that id.
systemOf(index, 'sol-p1')?.name     // 'Sol'  — from a planet
systemOf(index, 'sol')?.name        // 'Sol'  — a system returns itself
systemOf(index, 'ghost')            // null

lanesOf

function
Every hyperlane that touches a system, in either direction.
function lanesOf(galaxy: Galaxy, systemId: string): Hyperlane[]

neighboursOf

function
Systems reachable in one jump. There is no guarantee there are any — your network decides.
function neighboursOf(galaxy: Galaxy, systemId: string): StarSystem[]

stationsOf

function
Every station in a system, flattened across all its planets. Takes a system, not a galaxy.
function stationsOf(system: StarSystem): Station[]

findRoute

function
Shortest path between two systems, weighted by Hyperlane.length— not by geometry. Override a lane's length and you have a travel-cost model.
function findRoute(
  galaxy: Galaxy,
  fromId: string,
  toId: string,
): Route | null

Returns

{ systemIds, distance, jumps }, or null if either system does not exist or nothing links them. Routing a system to itself returns a zero-length route, not null.

Complexity

Dijkstra with a linear-scan priority queue. Fine to a few thousand systems; past that, index the graph once and swap in a binary heap.
findRoute(galaxy, 'a', 'c')
// { systemIds: ['a', 'b', 'c'], distance: 200, jumps: 2 }

findRoute(galaxy, 'a', 'island')   // null — the network is split

connectedComponents

function
Split the network into groups of systems that can reach each other.
function connectedComponents(galaxy: Galaxy): string[][]

Returns

The components, largest first. A healthy galaxy returns exactly one.
const parts = connectedComponents(galaxy)
if (parts.length > 1) {
  throw new Error(`Stranded systems: ${parts.slice(1).flat().join(', ')}`)
}

Geometry

Conventions throughout: the star sits at (0, 0), the Y axis points down (as in SVG), and angles are degrees measured clockwise from the positive X axis.

systemPositions

function
Every position in a system at time t, keyed by id. Stations are placed relative to their planet, so they are carried along its orbit. Gates and asteroid fields never move.
function systemPositions(system: StarSystem, t: number): Map<string, Vec2>

Parameters

systemStarSystem
One system, not the galaxy.
tnumber
Simulation time, in seconds.

Returns

A Map covering the star, every planet, every station, every gate and every asteroid field.

Nothing to cache

One pass over the objects of one system — a few dozen. Call it every frame.
const at = systemPositions(system, 6)
at.get('sol-p0')       // { x: -90, y: 0 }
at.get('sol-p1-st0')   // the station, carried by its planet

orbitPos

function
Position on a circular orbit at a given time. The primitive underneath systemPositions — reach for it to place something the model does not know about.
function orbitPos(
  orbit: number,
  angleDeg: number,
  period: number,
  t: number,
): Vec2

Parameters

orbitnumber
Orbit radius.
angleDegnumber
Starting angle, at t = 0.
periodnumber
Seconds per full orbit. 0 means it never moves.
tnumber
Simulation time, in seconds.
orbitPos(100, 0, 0, 0)    // { x: 100, y: 0 }
orbitPos(100, 90, 0, 0)   // { x: 0, y: 100 }  — 90° is straight down
orbitPos(100, 0, 8, 2)    // { x: 0, y: 100 }  — a quarter of an 8s orbit

fieldCenter

function
The anchor point of an asteroid field: the middle of the arc for a belt, the centre for a cluster.
function fieldCenter(field: AsteroidField): Vec2

toRad, round

function
Two helpers the library uses internally and exports for the same boundary work. round() is what keeps 0.30000000000000004 out of your save files.
const toRad: (d: number) => number
const round: (n: number, d?: number) => number   // d defaults to 2

Round at the edges

Rounding inside a computation accumulates error instead of removing it. Do it when you serialize or hand a number to a renderer.
toRad(180)                   // 3.141592653589793
round(0.30000000000000004)   // 0.3
round(1.23456, 3)            // 1.235

Randomness

createRng

function
A deterministic source of randomness from any string. Two instances made from the same seed produce the same sequence. The library never calls Math.random().
function createRng(seed: string): Rng

interface Rng {
  next(): number
  rand(min: number, max: number): number
  randInt(min: number, max: number): number
  pick<T>(arr: readonly T[]): T
  chance(p: number): boolean
  weighted<T extends { w: number }>(table: readonly T[]): T
}

Not cryptographic

mulberry32 is a content generator. Never use it for tokens, keys, or anything an adversary benefits from predicting.
const rng = createRng('my-game-001')

rng.next()                 // 0.721389…   float in [0, 1)
rng.randInt(1, 6)          // 4 — both bounds included
rng.pick(['a', 'b', 'c'])  // 'a'
rng.chance(0.5)            // true
rng.weighted([{ w: 9, id: 'common' }, { w: 1, id: 'rare' }])

hashSeed, mulberry32

function
The two primitives underneath. createRng(seed) is exactly mulberry32(hashSeed(seed)) plus the six conveniences.
function hashSeed(str: string): number          // FNV-1a, unsigned 32-bit
function mulberry32(a: number): () => number   // the raw generator
hashSeed('sol')       // 3795205537
const next = mulberry32(42)
next()                // 0.601104…

Types

Every type is exported from the root. The Data model covers them field by field; this is the index.

FieldTypeDescription
GalaxyInputinputThe root of what you write. Everything else nests inside it.
…InputinputSystemInput, StarInput, PlanetInput, StationInput, GateInput, LaneInput, LocalRouteInput, FactionInput, AsteroidBeltInput, AsteroidClusterInput, AsteroidFieldInput.
Galaxyoutput{ meta, systems, lanes } — what createGalaxy() returns.
StarSystem, Star, Planet, Station, GateoutputThe completed forms: every id filled in, every collection present.
AsteroidFieldoutputAsteroidBelt | AsteroidCluster, discriminated by kind.
Hyperlane, LocalRoute, ObjectRef, Faction, GalaxyMetaoutputThe network, the local links, the references and the counters.
IndexEntryqueryA discriminated union on kind. Checking it narrows obj with no cast.
GalaxyIndex, SystemsByIdqueryThe two ReadonlyMaps the index helpers return.
Routequery{ systemIds, distance, jumps }.
GalaxyIssuevalidation{ path, message }.
Vec2, Rockgeometry{ x, y }, and the [x, y, radius] tuple of an asteroid.
ObjectKindunion'system' | 'star' | 'planet' | 'station' | 'gate' | 'field' — closed, unlike the ones below.
PlanetType, StarClass, StationType, FactionId, Richness, GateStatus, RouteTypeopen unionSuggested values appear in autocomplete; any other string is accepted. See Open unions.
Rng, WeightedrandomnessThe generator interface, and the { w: number } constraint of a weighted table.
CreateGalaxyOptionsoptions{ validate?: boolean }.