astro - Elite-Dangerous-Almanac/Almanac-Core GitHub Wiki

@elite-dangerous-almanac/core / astro

astro

Astrophysical data and calculations for the Elite Dangerous galaxy.

This entry point re-exports the astro feature area's general API. Every symbol here is also reachable from its own module, so bundlers can drop anything you do not use. The 42-region coordinate lookup stays on ./codex-region-lookup: its map geometry is large enough that native ESM and namespace consumers must opt into it explicitly.

Start with ProceduralSystem — it composes the pieces below into one immutable handle (name ⇄ id64, sector, mass code, hand-authored regions). Drop to the individual functions when you need just one calculation.

A note on the word "region". It means four different things here; the exports are grouped to keep them apart:

None of those is the nebula catalogue — the nebulae themselves, and where they are: nearestNebulae & co. over REAL_NEBULAE, PROCGEN_NEBULAE, and — on their own subpaths, so importing one never bundles the rest — PLANETARY_NEBULAE and ALL_NEBULAE.

Bodies arrive as the journal writes them. BodyScanEvent is the game's Scan event typed field for field — the record every journal reader, EDDN relay and body database already speaks — so a parsed line is already the right type and needs no adapter. Its SystemAddress is the same SystemAddressInput every address entry point takes.

The body calculations read BodyProperties, the physical half of that. A BodyScanEvent is one, so a parsed line goes straight in; so does a record rebuilt from a database, because none of the journal's bookkeeping — event, BodyID, the discovery flags — is required. They work in the journal's own units and answer null rather than guess: bulkDensity and rocheLimits on ./body-physics, orbitExtents and spinOrbitResonance on ./body-orbit, ringDynamics on ./body-rings, classifyNeutronStar and mainSequenceLifetime on ./star-physics. A calculation comparing a body with the one it orbits takes both, since a scan names its parent only by BodyID.

Coordinate spaces have different shapes. GalacticPosition is {x, y, z} in light-years with Sol at the origin — what the journal, EDSM and Spansh report, and what findHandAuthoredRegionAt and nearestNebulae take. SectorGridPosition uses {sectorX, sectorY, sectorZ} integer indices on the 1280 ly naming grid, which is what sectorNameFromGridPosition takes. The distinct axis names prevent accidentally passing light-years as sector indices. Convert a real position with sectorGridPositionFromGalacticPosition (or go straight to sectorNameFromGalacticPosition).

findCodexRegionAt reads only {x, z}, because the region map is an X/Z projection; it takes a GalacticPosition as it comes and ignores the y.

Permit locks are six similarly-named lookups; permitLockForSystemName is the one to start from (it answers for both kinds of lock, from a name alone).

Examples

Start here: a name in, an id64 out, and back again.

import { ProceduralSystem } from '@elite-dangerous-almanac/core/astro/procedural-system';

const system = ProceduralSystem.fromName('Synuefe EN-H d11-96');
system?.systemAddress; // -> 3309179996515n
system?.namingRegionName; // -> 'Synuefe'
system?.massCode; // -> 'd'

ProceduralSystem.fromSystemAddress(3309179996515n).name; // -> 'Synuefe EN-H d11-96'

fromName returns null for a string that is not a procedural name — Sol and the other hand-named systems are deliberately outside this type.

The four meanings of "region", all answered for one position — the Pleiades. They disagree, which is the point: each names a different thing.

import { sectorNameFromGalacticPosition } from '@elite-dangerous-almanac/core/astro/galaxy-grid';
import { resolveNamingRegionOrigin } from '@elite-dangerous-almanac/core/astro/naming-region-origins';
import { findHandAuthoredRegionAt } from '@elite-dangerous-almanac/core/astro/hand-authored-regions';
import { findCodexRegionAt } from '@elite-dangerous-almanac/core/astro/codex-region-lookup';

const position = { x: -81.625, y: -151.3125, z: -376.0625 };

sectorNameFromGalacticPosition(position); // -> 'Synuefai'          procedural sector
findHandAuthoredRegionAt(position)?.name; // -> 'Pleiades Sector'   hand-authored region
findCodexRegionAt(position)?.name; // -> 'Inner Orion Spur'  codex region
resolveNamingRegionOrigin('Synuefai')?.x0; // -> 1556480            naming-region origin

And the nebula catalogue, which is none of those — it answers "what is near here":

import { nearestNebulae } from '@elite-dangerous-almanac/core/astro/nebulae';
import { REAL_NEBULAE } from '@elite-dangerous-almanac/core/astro/nebulae-real';

const position = { x: -81.625, y: -151.3125, z: -376.0625 };

nearestNebulae(position, REAL_NEBULAE, 2).map((n) => [n.name, n.distanceLy]);
// -> [['Pleiades', 32.74…], ['Taurus Dark Region', 91.07…]]

The catalogue argument is required here rather than defaulted: ALL_NEBULAE is ~432 KiB, so importing it must be your decision.

The two coordinate spaces, side by side. Light-years and sector indices are both three numbers, so the axis names are what stop you mixing them up.

import { sectorGridPositionFromGalacticPosition } from '@elite-dangerous-almanac/core/astro/galaxy-grid';
import { sectorNameFromGridPosition } from '@elite-dangerous-almanac/core/astro/sector-name';

// GalacticPosition: light-years from Sol — what a journal or EDSM gives you.
const position = { x: -81.625, y: -151.3125, z: -376.0625 };

// SectorGridPosition: integer indices on the 1280 ly naming grid.
const grid = sectorGridPositionFromGalacticPosition(position);
grid; // -> { sectorX: 38, sectorY: 31, sectorZ: 18 }

sectorNameFromGridPosition(grid); // -> 'Synuefai'

Permit locks come in two kinds, and one lookup answers for both from a name alone.

import { permitLockForSystemName } from '@elite-dangerous-almanac/core/astro/permit-locks';

permitLockForSystemName('Sol')?.kind; // -> 'system'   individually locked
permitLockForSystemName('Col 70 Sector AB-C d1-23')?.kind; // -> 'region'   the whole region is locked
permitLockForSystemName('Synuefe EN-H d11-96'); // -> null       not locked at all

Bodies: a journal Scan line goes in as it comes, and each calculation reads only the fields it needs — so a record with just a mass and a radius works too.

import { bulkDensity, rocheLimits } from '@elite-dangerous-almanac/core/astro/body-physics';
import { orbitExtents } from '@elite-dangerous-almanac/core/astro/body-orbit';

const moon = { MassEM: 0.0123, Radius: 1_737_400, SemiMajorAxis: 3.844e8, Eccentricity: 0.0549 };
const earth = { MassEM: 1, Radius: 6_371_000 };

bulkDensity(moon); // -> 3343.7…
orbitExtents(moon)?.periapsis; // -> 363296440
rocheLimits(moon, earth)?.rigid; // -> 9483500.2…

Classes

Interfaces

Type Aliases

Variables

Functions

⚠️ **GitHub.com Fallback** ⚠️