spacecraft

package
v0.36.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package spacecraft holds the Spacecraft type and its mutable runtime state (current primary + state vector + fuel). Physics in internal/physics operates on StateVector directly; this package is the glue between a named vessel, its current primary body, and the propagator.

Index

Constants

View Source
const (
	ComponentEngine      = "engine"       // thrust + Isp + fuel chemistry
	ComponentTank        = "tank"         // fuel capacity + fuel chemistry
	ComponentCommandCore = "command-core" // control point (crewed/probe) + recovery
	ComponentAntenna     = "antenna"      // comms hardware (direct/relay)
	ComponentStructure   = "structure"    // inert dry mass (adapter / fairing / ballast)
)

Component-kind constants (ADR 0029 §2). The five kinds that ship this cycle; cargo-hold is deferred to the inert-cargo cycle (ADR 0029 §6).

View Source
const (
	AntennaRangeDirectBasic   = 1.0e7  // near-Earth (LEO → geostationary), no forwarding
	AntennaRangeRelayCislunar = 1.0e9  // Earth/Moon workhorse, forwards
	AntennaRangeDeepSpace     = 6.0e11 // Mars-class near opposition (far-interplanetary tuning deferred, #182)
)

Antenna tier rated ranges in metres (ADR 0027 §2 amendment, v0.22.x): the reference tiers of the CommNet combinability model, where a link reaches √(rangeₐ · range_b). These mirror the values authored in the catalog (parts.json / ground_stations.json); kept here as named constants for the default-probe backfill (EnsureCommandSource) and the save-compat re-ranging of a pre-amendment save's antenna. Tier behaviour (vs the home DSN ≈ 5e9 m): direct-basic reaches LEO→geostationary; relay-cislunar reaches the Moon and forwards (the Relay-Tug); deep-space reaches Mars-class distances.

View Source
const (
	FuelTypeKerolox    = "kerolox"    // RP-1 + LOX (F-1, Merlin) — orange
	FuelTypeHydrolox   = "hydrolox"   // LH2 + LOX (J-2, RS-25, RL-10) — pale cyan
	FuelTypeHypergolic = "hypergolic" // Aerozine 50 + N2O4 (LM, SPS) — yellow-amber
	FuelTypeSolid      = "solid"      // APCP (SLS SRB) — orange-red
)

FuelType constants (v0.11.5). The renderer maps each value to a flame colour in internal/tui/screens/launch_sprite.go; the in-game vocabulary lives in CONTEXT.md under "Maneuver & thrust".

View Source
const (
	CommandNone   = ""       // not a command source (tank / engine / spent booster)
	CommandCrewed = "crewed" // a crewed pod — flown by its crew, never comms-gated
	CommandProbe  = "probe"  // an uncrewed probe core — needs a connection to be commanded
)

Command-source constants (v0.23 / ADR 0027). A stage's CommandSource declares whether it is a control point and, if so, whether crewed. Empty ("") means none (the stage is not a command source). A vessel is Controllable iff ≥1 stage is a command source; Crewed iff any is crewed (else it is an unmanned probe, comms-gated in the connectivity slices).

View Source
const (
	AntennaNone   = ""       // no antenna
	AntennaDirect = "direct" // can use the network, cannot forward for others
	AntennaRelay  = "relay"  // can use AND forward (a comm-sat / ground-station relay)
)

Antenna-kind constants (v0.23 / ADR 0027). A stage's AntennaKind declares its comms hardware: a direct antenna can use the network (reach a ground station, possibly via relays) but cannot forward traffic; a relay antenna can also forward for other vessels. Empty ("") means no antenna.

View Source
const (
	StageModuleSICID      = "s-ic"      // Saturn V first stage (F-1 cluster)
	StageModuleSIIID      = "s-ii"      // Saturn V second stage (J-2 cluster)
	StageModuleSIVBID     = "s-ivb"     // S-IVB / J-2 insertion + transfer
	StageModuleICPSID     = "icps"      // RL-10 low-TWR transfer stage
	StageModuleSRBID      = "srb"       // SLS twin 5-segment solids
	StageModuleCoreRS25ID = "core-rs25" // SLS core (4× RS-25)
	StageModuleF9S1ID     = "f9-s1"     // Falcon 9 first stage (9× Merlin 1D)
	StageModuleF9S2ID     = "f9-s2"     // Falcon 9 second stage (Merlin Vac)
	StageModuleLanderID   = "lander"    // LM-derived throttleable descent (single-stage)
	// v0.12 Slice 2 / ADR 0007: the 2-stage Lander split — descent (legs,
	// soft-land, surface-stage candidate) + ascent (no legs, returns to
	// orbit). Used by the split standalone Lander loadout and the Apollo
	// Stack's LM tier. v0.13: BuildModule expands the configurator's single
	// "lander" pick into this [Descent, Ascent] pair, so a custom stack adds
	// the LM as one vessel rather than as two separate parts.
	StageModuleLanderDescentID = "lander-descent"
	StageModuleLanderAscentID  = "lander-ascent"
	StageModuleCSMID           = "csm" // Apollo Command/Service Module (SPS)
	// v0.12 / ADR 0009: the fused CSM split into a propulsive Service
	// Module (SPS engine + all propellant; does LOI/TEI) and a passive
	// Command Module (engineless parachute capsule; the surviving core).
	// Like lander-descent/ascent above, both live in the catalog map so
	// the Apollo-Stack loadout's by-Name sprite/flag lookups resolve
	// them, but are intentionally left out of StageCatalogOrder — the
	// configurator still offers the single fused "csm" module. They do
	// NOT alias to "csm": the csm entry carries hasParachute (it survives
	// to re-entry as one piece), but the split SM must NOT — only the CM
	// carries the chute.
	StageModuleServiceModuleID = "service-module"
	StageModuleCommandModuleID = "command-module"
	// StageModuleApolloCSMLMID (v0.14 / ADR 0011) is a COMPOSITE module
	// pick: BuildModule expands it to [SM, CM, Descent, Ascent] and
	// ModuleNosePayloadTop reports that the top 2 (the LM) form a docked
	// nose payload. Picking it in the configurator and spawning lands the
	// post-transposition Apollo composite — SM firing core, LM an
	// Undock-able nose payload — already assembled, no flip to fly.
	StageModuleApolloCSMLMID = "csm-lm"
	// v0.12 Slice 3 / ADR 0008: standalone re-entry capsule — single
	// command-module-class stage with a parachute, no engine landing.
	StageModuleCapsuleID = "capsule"
	StageModuleRCSTugID  = "rcs-tug" // pure-monoprop proximity-ops module
)

Catalog stage IDs.

View Source
const ChuteDeployedBC = 0.3

ChuteDeployedBC is the fixed effective Ballistic Coefficient (C_D · A / m, m²/kg) a vessel reports while its parachute is deployed — an *absolute replace* of the normal BC chain, not a multiplier. The canopy area physically dominates the capsule's own drag, so the base BC is irrelevant; fixing BC makes terminal velocity predictable and mass-independent (the mass term lives inside BC = C_D·A/m). At Earth sea level (ρ₀ ≈ 1.2, g ≈ 9.81) the drag-model terminal-velocity relation v_term = √(2g / (ρ · BC)) gives ≈ 7.4 m/s — comfortably under CrashVCritMps (10 m/s). Retunable from playtest like V_CRIT.

View Source
const DefaultBallisticCoefficient = 0.01

DefaultBallisticCoefficient is the v0.8.4 baseline (C_D · A / m) for an S-IVB-1-class craft in m²/kg. Used as the fallback when Spacecraft.BallisticCoefficient is zero — legacy saves and any loadout that hasn't been tuned yet inherit this.

View Source
const DefaultProbeAntennaRangeM = AntennaRangeDirectBasic

DefaultProbeAntennaRangeM is the basic telemetry antenna EnsureCommandSource backfills onto any vessel that carries none (ADR 0027): a bare launch vehicle / un-annotated custom stack / crewed pod has a guidance + telemetry antenna in reality, so without one a probe could never establish a connection and would be permanently uncommandable under the command gate. The direct-basic tier. (Name kept for back-compat; the default is no longer probe-only — every vessel gets it, v0.24.)

View Source
const DefaultSlewRateDegPerSec = 15.0

DefaultSlewRateDegPerSec is the attitude slew-rate cap applied to any loadout that does not override it (Loadout.SlewRateDegPerSec == 0) and to legacy/test craft built without a loadout. 15°/s ≈ 12 s for a 180° flip — visible and deliberate, but snappy enough not to be tedious (raised from 5°/s after v0.10.0 playtest). Cosine loss is still a real consequence the player times burns around. v0.10.0+.

View Source
const LoadoutApolloStackID = "Apollo-Stack"

LoadoutApolloStack is the v0.10.1+ full Apollo mission stack: Saturn-V launch chain with a Lunar Module + Command/Service Module payload on top. Stages bottom-first = [S-IC, S-II, S-IVB, Lander, CSM]. The decouple sequence is the real mission arc on top of the v0.9.1 staging machinery: drop S-IC → S-II → S-IVB (after the TLI burn) → Lander; the Lander spawns as its own controllable slate craft (payload separation) and the CSM is left as the player's surviving core to fly the rendezvous / return. The first three stages reuse the canonical Saturn-V tuning so ascent flies identically.

View Source
const LoadoutCapsuleID = "Capsule"

LoadoutCapsule is the v0.12 Slice 3 (ADR 0008) standalone re-entry capsule: a single command-module-class stage with a recovery parachute and no engine landing capability. The clean, directly- spawnable test vehicle for the chute subsystem — spawn it, de-orbit, `space` to arm, watch it auto-deploy and splash down under V_CRIT without crashing. The CSM is only reachable via the full Apollo Stack → orbit → four decouples, too slow to iterate on.

View Source
const LoadoutFalcon9ID = "Falcon-9"

LoadoutFalcon9 is the 2-stage SpaceX Falcon 9 Block 5. v0.9.4+. 9× Merlin 1D first stage → 1× Merlin Vacuum second stage. Higher TWR than the heavy-lift options (~1.4 at lift-off) and a smaller stack — handles like a sport rocket compared to the Saturn V / SLS heavies.

View Source
const LoadoutICPSID = "ICPS"

LoadoutICPS is the v0.5.6 RL-10 low-TWR transfer stage.

View Source
const LoadoutKernStackID = "Kern-Stack"

LoadoutKernStack is the v0.15 / ADR 0014 scale-matched vehicle for the stripped-back Lumen system: a simplified 4-stage Apollo analog — Boost → Transfer → single Lander → parachute Pod, bottom-first — sized to a ~6 km/s Cursor-landing-and-return budget on Lumen's ~1/10-linear scale (~3.4 km/s to Kern orbit vs Sol's ~9.4). It is the only Loadout tagged bodies.ScaleStrippedBack, so the spawn form's Δv-to-orbit hint flags it as best-for-Lumen — but, per ADR 0014, the tag never filters: the Kern Stack can still be spawned in Sol (where it can't reach orbit) and the real fleet in Lumen. DecouplePlan [1,1,1] drops Boost, Transfer and Lander one at a time; the engineless parachute Pod is the surviving core that splashes down (ADR 0008 recovery model). Engine Isp values are KSP stock numbers collapsed to the model's single-Isp-per-stage convention (no altitude-varying Isp): Mainsail (Boost, sea-level 285), Poodle (Transfer, vac 350), Terrier (Lander, vac 345).

View Source
const LoadoutLanderID = "Lander"

LoadoutLander is the throttleable descent-stage profile.

View Source
const LoadoutRCSTugID = "RCS-tug"

LoadoutRCSTug is the pure-monoprop proximity-ops vehicle.

View Source
const LoadoutSIVB1ID = "S-IVB-1"

LoadoutS_IVB1 is the v0.5.13+ Apollo S-IVB / J-2 default.

View Source
const LoadoutSLSBlock1ID = "SLS-Block1"

LoadoutSLSBlock1 is the 3-stage NASA Space Launch System Block 1. v0.9.4+. Twin 5-segment solid boosters (SRBs) → 4× RS-25 core stage → ICPS. Same low-TWR upper-stage shape as Saturn V (Core TWR ~0.87 after SRB sep), so gameplay translates: continuous-burn ascent, no coast-and-circularize.

View Source
const LoadoutSaturnVID = "Saturn-V"

LoadoutSaturnV is the 3-stage Apollo launch vehicle. v0.9.1+.

View Source
const PitchTrimStepRad = math.Pi / 36

PitchTrimStepRad is the per-keypress pitch trim adjustment in radians. v0.16: 5° (= π/36) — finer control for the gravity turn. History: v0.9.2 shipped at 5°, v0.9.2.1 bumped to 10° because a Saturn V's gravity turn needed too many `>` taps to get going; the 5° step is restored per playtest preference (the smaller stripped-back Lumen vehicles steer better with finer granularity, and held `>` ramps continuously at the terminal key-repeat rate for big pitch-overs).

View Source
const RCSDvQuantum = 0.1

RCSDvQuantum is the per-pulse Δv applied when an attitude key fires in RCS mode. Sized at 0.1 m/s per scoping decision #7 — small enough for sub-m/s precision proximity work, large enough that a held key at terminal-default ~5 Hz key-repeat delivers usable corrections in a few seconds. v0.8.0+.

View Source
const RCSFineLevels = 3

RCSFineLevels is how many per-pulse magnitudes the RCS fine-trim cycle steps through before wrapping: the coarse RCSDvQuantum, then ÷10 and ÷100 (0.1 → 0.01 → 0.001 m/s). v0.24.5+.

View Source
const RoleJettisonedStage = "jettisoned-stage"

RoleJettisonedStage is the Role stamped on a spent stage popped into its own passive craft by staging (World.StageActive → buildJettisonedCraft). It marks debris: EnsureCommandSource never backfills it, so a spent booster stays uncommandable across construction and save-load (ADR 0027). Lives here so the sim staging code and the save-load backfill agree on one value rather than two magic strings.

View Source
const VesselGlyph = "➤"

VesselGlyph is the single marker every craft collapses to on the orbit map (ADR 0020). Vessels are distinguished by Colour, not shape: the orbital-marker vocabulary (▲ apo / ▼ peri / ◇ AN / ◆ DN / ⊕ perilune / ✕ closest / Δ node) owns the geometric shapes, so a vessel must not reuse any of them or the player can't tell a ship from an apsis. ➤ (U+27A4, Dingbats — same well-supported block as the marker ✕) is a heading-chevron reserved for craft. Every Loadout/Stage glyph below references this constant; do not hand-pick a per-craft glyph.

Variables

AllBurnModes is the cycle order for the planner UI. v0.9.3+: the four target-relative modes append after the body-frame six. Surface modes stay out — planted nodes can't predict future v_surface.

The maneuver form skips target-relative entries when World.Target.Kind != TargetCraft (no defined direction without a craft target).

AllTriggerEvents lists the trigger modes in canonical UI cycle order.

View Source
var Components = buildComponents()

Components indexes the embedded component catalog by ID — the palette the VAB composes from (ADR 0029 §5). Resolved at package-var init from the embedded data/components.json (mirroring Loadouts / StageCatalog), and refreshed with the user overlay by LoadCatalogOverlay at startup. Empty in S1's stub; starter content lands in S4.

View Source
var Loadouts, LoadoutOrder = buildLoadouts()

Loadouts indexes the launch catalog by ID, and LoadoutOrder is the canonical UI cycle order (the v0.8.2+ spawn form cycles through it). v0.23 / ADR 0026 (C1-3): both are now resolved from the embedded data/loadouts.json + data/parts.json at init via buildLoadouts, rather than hardcoded Go literals — the loadout catalog is data, the last hardcoded catalog in the project to move. The resolved Loadout shape (Stages bottom-first, RCS derived from dry mass, per-loadout plans) and every reader (NewFromLoadout / LookupLoadout / the spawn + staging + save paths) are unchanged: only the source of the data moved, so every loadout flies byte-identical (golden-tested). User overlays merge in at a higher layer (C1-4).

View Source
var StageCatalog = buildStageCatalog()

StageCatalog indexes the parts library by ID. v0.23 / ADR 0026 (C1-2): the data now lives in the embedded data/parts.json (loaded at package init via buildStageCatalog) rather than a hardcoded Go literal — the first cut toward the normalized, modder-overridable parts catalog. The in-memory StageModule shape and every reader (BuildStage / BuildModule / the catalog*ByName loadout helpers) are unchanged: the migration moved the *source* of the data, not its representation or behaviour, so the catalog flies byte-identical (golden-tested). The numbers mirror the inline loadout literals in loadouts.go (see file-level note).

Embedded-catalog load is deliberately fatal: a malformed shipped parts.json is a build/programmer error, not a recoverable runtime condition (mirrors regexp.MustCompile). The *user overlay* path — skip-bad-with-warning — is wired in at a higher layer (C1-4), not here, so BuildStage stays I/O-free and the golden tests stay deterministic.

StageCatalogOrder is the configurator's canonical cycle order — roughly bottom-of-stack (heavy boosters) to top (payload), so a player building a stack from scratch naturally walks the list adding a booster first and a payload last.

Functions

func ApplyPitchTrim added in v0.9.2

func ApplyPitchTrim(dir, r, spinAxis orbital.Vec3, pitchRad float64) orbital.Vec3

ApplyPitchTrim rotates dir about the local-north axis at position r by pitchRad (radians, positive = east). Used by BurnDirection to fold the player's pitch-trim setting into any burn mode's natural direction. Public so tests can exercise the rotation math directly.

Frame:

up    = r̂                          (local vertical)
east  = unit(spinAxis × up)         (local east on the body)
north = up × east                   (right-handed local frame)

Rotation about north tilts the thrust vector east (+pitch) or west (-pitch) without changing the heading component. At the poles (where east is undefined) the rotation is a no-op.

spinAxis is the body's true spin axis in world coordinates (tilted per AxialTilt + AxialAzimuth, matching render.BodyRotationAxisWorld). Pass orbital.Vec3{Z: 1} for an un-tilted body to get the legacy pre-v0.9.4 behaviour.

v0.9.2+. v0.9.4+: spin-axis param so the trim's east axis matches the launchpad spawn frame on tilted bodies (Earth: 23.5°).

func DefaultAntennaRangeForKind added in v0.22.2

func DefaultAntennaRangeForKind(kind string) float64

DefaultAntennaRangeForKind returns the reference rated range for an antenna kind. Used to re-range a pre-amendment save's antenna: those saves stored the legacy antenna_power_w (now an ignored key), so the loaded range is 0 — the save loader maps the surviving kind back to its tier. relay → cislunar, direct → basic, none → 0.

func DefaultRCSLoadout added in v0.8.0

func DefaultRCSLoadout(dryMass float64) (monoprop, capacity, thrust, isp float64)

DefaultRCSLoadout returns canonical (monoprop, capacity, thrust, isp) for a craft of the given dry mass. Linear scaling per v0.8 plan scoping decision #8: RCSThrust = k_T · m_dry, capacity = k_M · m_dry, tuned so a default S-IVB-1-class craft (11000 kg dry + 40000 kg fuel) gets ~30 m/s of RCS Δv budget — enough for proximity ops without being twitchy.

kCap was 50/11000 in the initial v0.8.0 cut; the v0.8 plan's "~28 m/s budget" formula `Isp · g₀ · ln(m₀/m_dry)` conflated total fuel ejection with monoprop ejection, and the realised budget at 50 kg was only ~2 m/s. To actually hit the planned ~30 m/s on a 51 t wet craft at Isp=220 the monoprop pool needs ~720 kg (a 1.4 % mass fraction — physically realistic). v0.8.0+.

func DeleteDesign added in v0.24.0

func DeleteDesign(id string) error

DeleteDesign removes a design file from the store. Returns an error if no design with that ID exists (so the VAB can report a stale delete).

func DesignsDir added in v0.24.0

func DesignsDir() string

DesignsDir resolves the app-managed designs directory: $XDG_CONFIG_HOME/terminal-space-program/designs (or ~/.config/terminal-space-program/designs when XDG is unset). A sibling of the modder overlay's loadouts/ dir but a DISTINCT namespace (ADR 0029 §4) so app-written designs never collide with hand-authored mods.

func DirectionUnit

func DirectionUnit(mode BurnMode, r, v orbital.Vec3) orbital.Vec3

DirectionUnit returns a unit vector for the given burn mode given the craft's current (r, v) — primary-relative. Returns the zero vector if r or v is degenerate (can't define the frame).

Target-relative modes (BurnTargetPrograde / Retrograde / BurnTarget / AntiTarget) are not handled here — they require target craft state. Callers with a target use DirectionUnitTarget; pure-function callers without target state in scope (predictor, AllBurnModes preview math when no target is set) get the zero vector + degraded behaviour.

func DirectionUnitTarget added in v0.9.3

func DirectionUnitTarget(mode BurnMode, rA, vA, rT, vT orbital.Vec3) orbital.Vec3

DirectionUnitTarget returns the unit thrust direction for the four target-relative modes (v0.9.3+) given the active and target craft states in the SAME frame. Both states should be primary-relative when the two craft share a primary, or fully inertial when they don't — the world layer (World.targetStateRelativeToActivePrimary) handles that conversion.

Non-target modes fall through to DirectionUnit(mode, rA, vA), so this is safe to call from any thrust-direction site that wants uniform mode handling.

Returns the zero vector when the relative quantity is degenerate (identical positions for BurnTarget / AntiTarget; identical velocities for BurnTargetPrograde / Retrograde) or when called with zero rT, vT (no target resolved — caller passes zeros so the closure still constructs but the burn no-ops).

KSP convention (v0.10.3+): target-prograde = unit(v_active − v_target), target-retrograde = unit(v_target − v_active). Burn target-RETROGRADE to null v_rel during a rendezvous approach.

func EnsureCommandSource added in v0.22.0

func EnsureCommandSource(c *Spacecraft)

EnsureCommandSource makes a vessel network-ready (ADR 0027 defaulting rule). Two backfills, both idempotent and both skipped for jettisoned debris:

  1. Command source: if no stage already declares one, the surviving core (top stage) gets CommandCrewed when the vessel's role is a crewed pod, else CommandProbe.
  2. Antenna: if the whole vessel carries no antenna, the core gets a basic direct telemetry antenna — for EVERY commandable vessel, crewed or probe (v0.24). A probe without one would be permanently uncommandable under the comms gate; a crewed pod is never gated, so for it the antenna is presence-only (it appears on the CommNet, can't relay), but "all vessels carry an antenna" keeps the network model uniform.

The antenna backfill runs independently of the command-source one: a catalog crewed pod declares its own command_source (so step 1 is a no-op) yet still needs step 2 to put it on the network. Applied at vessel construction (NewFromLoadout / NewFromStages) and at save-load — NOT to jettisoned stages, so a spent booster with no core stays passive debris.

func IsCommandSource added in v0.22.0

func IsCommandSource(src string) bool

IsCommandSource reports whether a CommandSource value makes a stage a control point (crewed or probe — not none/empty/unknown).

func IsTargetRelativeMode added in v0.9.3

func IsTargetRelativeMode(m BurnMode) bool

IsTargetRelativeMode reports whether the given burn mode requires a target craft state. Used by m-form cycle gating and resolver dispatch. v0.9.3+.

func ListDesigns added in v0.24.0

func ListDesigns() ([]Design, []CatalogWarning)

ListDesigns reads every design in the store, skipping malformed files with a warning (the ADR 0026 skip-bad-with-warning convention). A missing store dir yields no designs and no warnings.

func LoadCatalogWithWarnings added in v0.22.0

func LoadCatalogWithWarnings() (map[string]Part, []LoadoutDef, []CatalogWarning, error)

LoadCatalogWithWarnings is the warning-aware variant. The returned warnings slice holds a CatalogWarning per user overlay file that failed to parse (and per composed Part that failed aggregation); embedded-catalog parse failures surface as a hard error (the embedded set must always load). This is the ADR 0026 "LoadAllWithWarnings style" entrypoint, mirroring bodies.LoadAllWithWarnings. The returned parts are aggregated: a composed Part (ADR 0029) carries its derived scalar stats, so every downstream reader (resolveLoadout / ToStage) sees flat fields exactly as for an atomic part.

func LoadDesign added in v0.24.0

func LoadDesign(id string) (Design, bool, []CatalogWarning)

LoadDesign returns the stored design with the given ID, or ok=false if none matches. Warnings from other malformed files in the store are surfaced so the caller can report them.

func ModuleNosePayloadTop added in v0.14.0

func ModuleNosePayloadTop(id string) int

ModuleNosePayloadTop reports how many of the TOP stages that BuildModule(id) produces form a docked nose payload — released by Undock, not Staging (the top-release counterpart to a Loadout's bottom-up DecouplePlan; ADR 0011). Non-composite modules return 0, so the configurator stacks them linearly. The "csm-lm" composite returns 2 (the LM = Descent + Ascent rides on the [SM, CM] core's nose). v0.14.

func NodeBurnDirection added in v0.10.4

func NodeBurnDirection(n ManeuverNode, r, v orbital.Vec3) orbital.Vec3

NodeBurnDirection resolves a planted node's unit thrust direction at state (r, v), handling BurnPlaneChange via the node's PlaneChangeRad before delegating every other mode to DirectionUnit. Target-relative nodes still resolve via DirectionUnitTarget at the call site — a plane-change node is never target-relative. v0.10.4+.

func SaveDesign added in v0.24.0

func SaveDesign(d Design) error

SaveDesign writes a design to the store, creating the dir on first use. Overwrites any existing design with the same ID (the VAB's save-over-name).

func SumDryMass added in v0.9.1

func SumDryMass(stages []Stage) float64

SumDryMass returns the total dry mass across every stage in kg.

func SumFuelCapacity added in v0.9.1

func SumFuelCapacity(stages []Stage) float64

SumFuelCapacity returns the total main-engine fuel-tank capacity across every stage in kg.

func SumFuelMass added in v0.9.1

func SumFuelMass(stages []Stage) float64

SumFuelMass returns the total main-engine propellant across every stage in kg.

func SumMonopropCap added in v0.9.1

func SumMonopropCap(stages []Stage) float64

SumMonopropCap returns the total RCS-tank capacity across every stage in kg.

func SumMonopropMass added in v0.9.1

func SumMonopropMass(stages []Stage) float64

SumMonopropMass returns the total RCS propellant across every stage in kg.

Types

type ActiveBurn added in v0.8.1

type ActiveBurn struct {
	Mode        BurnMode
	DVRemaining float64
	EndTime     time.Time
	PrimaryID   string
	Throttle    float64
	// TargetCraftID (v0.14.x / ADR 0012; was TargetCraftIdx) is the
	// stable Spacecraft.ID of the burn's target — zero means no target
	// bound. Populated when a target-relative finite-burn node fires;
	// the world's stepThrust resolves the target snapshot from this each
	// tick (via craftByID) so the burn keeps tracking even if the player
	// swaps World.Target or the slate shifts mid-burn.
	TargetCraftID uint64 `json:",omitempty"`
	// TargetGhostOwner (v0.28 S4) mirrors ManeuverNode.TargetGhostOwner
	// onto the running burn: when non-empty, TargetCraftID is a REMOTE
	// craft id resolved against the ghost slate each tick. Empty ⇒ local
	// ref, unchanged behaviour. Never persisted (save drops the ref).
	TargetGhostOwner string `json:",omitempty"`
	// PlaneChangeRad (v0.10.4+) carries the BurnPlaneChange rotation
	// angle from the firing node onto the running burn, so the
	// attitude/thrust path can resolve the tilted plane-change
	// direction each tick. Zero for non-plane-change burns.
	PlaneChangeRad float64 `json:",omitempty"`
	// BurnDirUnit (v0.12.x+) mirrors ManeuverNode.BurnDirUnit onto the
	// in-flight burn so the attitude/thrust path resolves the fixed
	// BurnVector direction each tick. Zero for non-BurnVector burns.
	BurnDirUnit orbital.Vec3 `json:",omitempty"`
}

ActiveBurn is the runtime state of an in-progress finite burn. Set by the dispatcher when a node with Duration>0 fires; cleared when DVRemaining hits zero or SimTime passes EndTime. v0.8.1+: lives on Spacecraft.ActiveBurn so each craft can run its own burn concurrently.

func (*ActiveBurn) DropGhostRef added in v0.28.0

func (b *ActiveBurn) DropGhostRef()

DropGhostRef clears a ghost target ref (owner + the remote craft id) from a running burn, mirroring ManeuverNode.DropGhostRef so a save mid-burn against a ghost never persists a session-local remote id. No-op for local-craft or untargeted burns. v0.28 S4.

func (ActiveBurn) TargetCraftIDValue added in v0.14.5

func (b ActiveBurn) TargetCraftIDValue() (uint64, bool)

TargetCraftIDValue mirrors ManeuverNode.TargetCraftIDValue — returns the stable ID the burn is bound to, or ok=false when no target was captured at fire time. Resolve via World.craftByID. v0.14.x / ADR 0012.

type Antenna added in v0.22.0

type Antenna struct {
	Kind   string  `json:"kind"`
	RangeM float64 `json:"range_m,omitempty"`
}

Antenna is the forward-compatible (ADR 0027) per-part antenna attribute: kind is "none" | "direct" | "relay"; a direct antenna can use the network, a relay antenna can also forward for others. range_m is the antenna's rated range in metres — the distance at which it reaches an identical antenna (the CommNet combinability model). Declared in the cycle-1 schema; cycle 2 reads it.

type BurnMode

type BurnMode int

BurnMode enumerates the six direction modes from plan §Phase 2.

const (
	BurnPrograde BurnMode = iota
	BurnRetrograde
	BurnNormalPlus  // orbit normal (+h direction)
	BurnNormalMinus // orbit normal (-h direction)
	BurnRadialOut   // away from primary
	BurnRadialIn    // toward primary

	// Surface-relative modes (v0.9.2+) — live SAS only, not planted-
	// node modes. Direction = ±(v_surface).Unit() where v_surface =
	// v - ω × r is the craft's velocity relative to the rotating
	// atmosphere. Useful for ascent gravity-turn flight: once the
	// craft has eastward velocity, BurnSurfacePrograde tracks it
	// even as the velocity vector pitches over from gravity drag,
	// and the autopilot rides the curving trajectory cleanly.
	//
	// Pre-launch (zero velocity) the surface direction is undefined;
	// BurnDirection returns the zero vector so the burn is a no-op.
	// The player nudges off the pad with pitch trim (BurnRadialOut +
	// trim east) and switches to BurnSurfacePrograde once velocity
	// is established.
	//
	// Not in AllBurnModes — surface modes don't appear in the m
	// planner's mode cycle, because planted nodes can't predict
	// future v_surface usefully.
	BurnSurfacePrograde
	BurnSurfaceRetrograde

	// Target-relative modes (v0.9.3+). Direction depends on the
	// active *and* target craft states in the same frame. KSP
	// convention (v0.10.3+, was swapped pre-v0.10.3):
	//
	//   BurnTargetPrograde   = unit(v_active − v_target)
	//   BurnTargetRetrograde = unit(v_target − v_active)
	//   BurnTarget           = unit(r_target − r_active)
	//   BurnAntiTarget       = unit(r_active − r_target)
	//
	// The velocity-relative pair is the primary tool for the manual
	// rendezvous loop — hold target-RETROGRADE to null v_rel during
	// approach (the closing axis), hold target-PROGRADE to widen the
	// relative-velocity gap on the way out. The position-relative pair
	// is for sub-m/s proximity-ops nudges after v_rel is nulled.
	//
	// All four require World.Target.Kind == TargetCraft. Without a
	// craft target, DirectionUnitTarget returns the zero vector and
	// the burn is a no-op (the live-closure path captures the world's
	// resolved target state once per step; the planted-node path
	// resolves at fire-time via ManeuverNode.TargetCraftIdx).
	BurnTargetPrograde
	BurnTargetRetrograde
	BurnTarget
	BurnAntiTarget

	// BurnPlaneChange (v0.10.4+) is a planted-node-only mode for a pure
	// orbital-plane rotation. A plane change must rotate the velocity
	// vector while preserving its magnitude — a pure orbit-normal burn
	// (BurnNormalPlus) cannot: adding Δv perpendicular to v always
	// speeds the craft up (|v_new| = √(v²+Δv²)), leaving the orbit
	// eccentric and the plane under-rotated. The correct direction is
	// the orbit normal tilted toward retrograde by half the rotation
	// angle; it depends on a continuous parameter the other modes don't
	// carry, so the rotation angle rides on the ManeuverNode / ActiveBurn
	// (PlaneChangeRad) and the direction is resolved via NodeBurnDirection
	// / BurnDirectionPlaneAware. Not in AllBurnModes — the player reaches
	// it only through the `I` auto-plant, never the m-form mode cycle.
	// Appended last so persisted node-mode ints keep their meaning.
	BurnPlaneChange

	// BurnVector (v0.12.x+) is a plant-only mode carrying an arbitrary
	// 3D thrust direction — the fused-Lambert departure Δv, which folds
	// eccentricity + apsis raise + plane change into one burn and so
	// can't be expressed by any derived mode (prograde / normal / radial
	// / plane-change). The unit direction is captured in the inertial
	// (primary-relative) frame at plant time and rides on the
	// ManeuverNode / ActiveBurn (BurnDirUnit); it is fixed for the burn
	// (the craft slews to it, lead-compensated like any node). Resolved
	// via NodeBurnDirection / BurnDirectionForBurn. Not in AllBurnModes —
	// the player reaches it only through the fused [H] auto-plant.
	// Appended last so persisted node-mode ints keep their meaning.
	BurnVector
)

func (BurnMode) String

func (m BurnMode) String() string

String is the label shown in the maneuver planner / HUD.

type Catalog added in v0.22.0

type Catalog struct {
	Components []Component  `json:"components,omitempty"`
	Parts      []Part       `json:"parts,omitempty"`
	Loadouts   []LoadoutDef `json:"loadouts,omitempty"`
}

Catalog is the on-disk envelope shared by the embedded data files and user overlay files: a list of components, parts and/or loadouts. All lists are optional, so a user file may add just components, just parts, just loadouts, or any mix (the modding path; v0.24 / ADR 0029 added the components list one level below parts).

type CatalogWarning added in v0.22.0

type CatalogWarning struct {
	Path string
	Err  error
}

CatalogWarning records a user overlay file that failed to load. Mirrors bodies.LoadWarning: embedded-catalog parse failures are hard errors (the embedded set must always load); user-file failures are warnings so one bad mod never rejects the whole catalog (ADR 0026 §3).

func LoadCatalogOverlay added in v0.22.0

func LoadCatalogOverlay() []CatalogWarning

LoadCatalogOverlay re-resolves the runtime catalogs (Loadouts, LoadoutOrder, StageCatalog) from the embedded data MERGED with the user overlay (the XDG loadouts/ dir), and returns warnings for any malformed user files (ADR 0026 §2/§3 — bodies-pattern overlay, skip-bad-with-warning).

Init (buildLoadouts / buildStageCatalog) loads the EMBEDDED catalog only, so package-var init and the golden tests stay deterministic. The app calls this once at startup to layer in user mods; tests that need only the embedded catalog skip it. User parts win on ID; user loadouts append (or replace on ID) after the embedded set, so LoadoutOrder lists them last. Embedded parse failures already panicked at init, so the error from LoadCatalogWithWarnings is not re-surfaced here.

func (CatalogWarning) Error added in v0.22.0

func (w CatalogWarning) Error() string

type ChuteState added in v0.12.4

type ChuteState int

ChuteState is the runtime deploy state of a Parachute. One-way: STOWED → ARMED → DEPLOYED, with DEPLOYED terminal (no re-stow / cut-away). There is deliberately no torn / failure state — the over-speed tear model was considered and cut during the grill (ADR 0008 Alternatives → tear model).

const (
	// ChuteStowed is the zero value: the capability may be present but
	// the chute has not been staged/armed.
	ChuteStowed ChuteState = iota
	// ChuteArmed: staged, waiting for enough air to inflate. Arming is
	// folded into the Stage (`space`) action — see ArmParachute.
	ChuteArmed
	// ChuteDeployed: inflated; the ChuteDeployedBC bump is active. Terminal.
	ChuteDeployed
)

func (ChuteState) String added in v0.12.4

func (s ChuteState) String() string

String renders a ChuteState for the HUD readout (STOWED / ARMED / DEPLOYED).

type Component added in v0.24.0

type Component struct {
	ID    string `json:"id"`
	Name  string `json:"name,omitempty"`
	Glyph string `json:"glyph,omitempty"`
	Color string `json:"color,omitempty"`

	// Description is an optional one-line blurb surfaced by the VAB part
	// inspector (ADR 0030 §7). Purely cosmetic catalog data — like Glyph /
	// Color it carries no save-hash weight (there is no parts-catalog hash;
	// ADR 0026 §4) and is omitempty so existing catalogs round-trip
	// byte-identical.
	Description string `json:"description,omitempty"`

	// Kind is one of the ComponentKind* constants; it selects which Stage
	// scalars this component contributes during aggregation.
	Kind string `json:"kind"`

	// DryMassKg is the component's empty mass in kg — additive across the
	// whole stage for every kind.
	DryMassKg float64 `json:"dry_mass_kg"`

	// Engine fields.
	ThrustN float64 `json:"thrust_n,omitempty"`
	IspS    float64 `json:"isp_s,omitempty"`

	// FuelType is shared by engine + tank: every fuelled component in a
	// stage must agree on chemistry (ADR 0029 §3, the single-fuel-pool
	// invariant). Empty contributes nothing to the chemistry check.
	FuelType string `json:"fuel_type,omitempty"`

	// Tank field.
	FuelCapacityKg float64 `json:"fuel_capacity_kg,omitempty"`

	// Command-core fields. CommandSource is CommandCrewed / CommandProbe
	// (ADR 0027); CanSoftLand / HasParachute are the optional recovery
	// capabilities a command core may carry.
	CommandSource string `json:"command_source,omitempty"`
	CanSoftLand   bool   `json:"can_soft_land,omitempty"`
	HasParachute  bool   `json:"has_parachute,omitempty"`

	// Antenna fields (ADR 0027): kind (AntennaDirect / AntennaRelay) and the
	// rated range in metres.
	AntennaKind string  `json:"antenna_kind,omitempty"`
	RangeM      float64 `json:"range_m,omitempty"`
}

Component is the finest catalog noun (ADR 0029 §1, the VAB cycle / Axis B cycle 4) — one level below the atomic Part. A Part may declare an optional list of component IDs; when present the Part's flat scalar stats are DERIVED by aggregation (aggregateComponents) instead of authored inline. This cashes in the forward-compat note ADR 0026 left on Part ("a Part can later declare itself a composition of finer components") with ZERO migration: an atomic Part (no Components) is untouched.

One Component contributes only to the Stage scalars its Kind owns (ADR 0029 §2):

engine        → ThrustN, IspS, FuelType, dry mass
tank          → FuelCapacityKg, FuelType, dry mass
command-core  → CommandSource, dry mass, optional soft-land / parachute
antenna       → AntennaKind, RangeM, dry mass
structure     → dry mass only (adapters, fairings, ballast)

Components are loaded through the existing ADR 0026 catalog loader (one more embedded file + user overlay, skip-bad-with-warning) — the modding path one level deeper. Visual fields (Glyph / Color) are cosmetic and carry no save-hash weight (there is no parts-catalog hash; ADR 0026 §4).

type Design added in v0.24.0

type Design struct {
	Loadout LoadoutDef
	Parts   []Part
}

Design is one saved custom vehicle: a Loadout plus the design-local composed Parts it references (each carrying a Components list; ADR 0029 §1). The on-disk form is a Catalog fragment {parts, loadouts:[one]}.

func (Design) ID added in v0.24.0

func (d Design) ID() string

ID / Name read the design's identity off its loadout. ID is the stable key (and file stem); Name is the display label.

func (Design) Name added in v0.24.0

func (d Design) Name() string

func (Design) Resolve added in v0.24.0

func (d Design) Resolve() (Loadout, []CatalogWarning)

Resolve turns a design into a flyable Loadout, resolved against the LIVE catalog (embedded + modder overlay) so its references — both design-local composed parts and existing atomic catalog parts — resolve correctly. The design's own composed parts are aggregated against the live component catalog (ADR 0029 §2). A dangling part reference is skip-bad (a warning + an empty Loadout), never a panic, so a hand-edited design file can't crash the app.

type DockedComponent added in v0.8.3

type DockedComponent struct {
	Name             string
	LoadoutID        string
	Role             string
	Glyph            string
	Color            string
	DryMass          float64
	FuelCapacity     float64
	MonopropCapacity float64
	Isp              float64
	Thrust           float64
	RCSThrust        float64
	RCSIsp           float64
	// CanSoftLand / HasParachute (v0.12 Slice 3, ADR 0008): the two
	// surface-arrival capability flags, captured so Undock can restore
	// them onto the rebuilt single-stage craft. Without this a chute-
	// bearing capsule (or a CanSoftLand lander) that docks then undocks
	// loses its capability — the restored Stages[0] would default false
	// and SyncFields would re-derive a false mirror, crashing the Earth
	// splashdown the chute exists for. (DockedComponent still doesn't
	// record the full per-stage breakdown — that broader gap is the
	// banked v0.9.1.x follow-up the Undock comment notes — but the
	// landing capabilities are cheap to carry and load-bearing.)
	CanSoftLand  bool
	HasParachute bool
	// Stages (v0.12 / ADR 0009): the component's full per-stage
	// breakdown, captured so Undock can restore a MULTI-stage craft
	// (e.g. the Apollo LM = Descent + Ascent released as a docked nose
	// payload after transposition). Closes the v0.9.1.x gap the flat
	// single-stage fields above couldn't cover. Empty/nil ⇒ Undock
	// falls back to the legacy single-stage prorate rebuild (old saves,
	// single-stage components). The recorded FuelMass is a dock-time
	// snapshot used only for the stage COUNT + identity; Undock reads
	// LIVE per-stage fuel from the composite's current Stages (the
	// firing Stages[0] is drained while docked, so the snapshot fuel
	// goes stale — see sim.Undock).
	Stages []Stage

	// Owner + CraftID (v0.28 S5 / ADR 0034 cross-player docking):
	// ownership provenance for a component in a cross-player stack. Owner
	// is the guest player's fingerprint when this component rides in
	// another player's stack; empty means it belongs to the World that
	// holds the composite (the current stack owner). CraftID is the
	// component's pre-dock stable Spacecraft.ID — for a guest component
	// it is the ID UndockGuest hands back so the craft returns to its
	// owner's World unchanged. Both zero-value/empty for a same-player
	// dock (the classic single-World composite), so old saves and local
	// docking are untouched; additive omitempty on the wire, no save
	// schema bump (matching the CanSoftLand / Stages precedent above).
	Owner   string
	CraftID uint64
}

DockedComponent is a snapshot of one pre-dock craft identity kept on its composite. Used by sim.Undock to restore the original vessels. State (position / velocity / nodes / burns) isn't preserved — the docked craft sit at the composite's state while joined; on undock they re-emerge near the composite's current state. v0.8.3+.

type EngineMode added in v0.8.0

type EngineMode int

EngineMode selects which propulsion system the manual-flight path drives: the main engine (high-thrust, fuel) or the monopropellant RCS thrusters (low-thrust, monoprop, pulse-fired). v0.8.0+.

Planted maneuver nodes always use the main engine; EngineMode only gates the live manual-flight inputs (attitude keys + b).

const (
	EngineMain EngineMode = iota
	EngineRCS
)

func (EngineMode) String added in v0.8.0

func (e EngineMode) String() string

String returns the HUD label for the engine mode.

type Loadout added in v0.8.2

type Loadout struct {
	ID    string
	Name  string
	Role  string
	Glyph string
	Color string
	// Stages is the per-stage breakdown, bottom-first. Required
	// (must be non-empty); a one-element Stages declares a
	// single-stage craft.
	Stages []Stage
	// DecouplePlan (v0.12 Slice 2 / ADR 0007) is an optional
	// bottom-up list of staging group sizes — how many contiguous
	// bottom Stages each staging press releases as a single craft.
	// Nil ⇒ all-ones (one Stage per press, the historical default).
	// The Apollo Stack declares [1,1,1,2] so the descent + ascent LM
	// pair extracts together as a 2-stage craft (see ADR 0007). Sum
	// of the plan should be < len(Stages) (the top stage is the
	// surviving core). Copied onto the Spacecraft in NewFromLoadout.
	DecouplePlan []int
	// NosePayloadPlan (v0.23 / ADR 0028 C3-3) bakes a carrier loadout's
	// top-release split into the catalog: each entry is a count of contiguous
	// TOP stages forming one docked nose payload, ordered top-down (the
	// top-release counterpart of DecouplePlan's bottom-up groups). Nil ⇒ a
	// plain linear craft. A carrier like "Comsat Carrier x3" declares [1,1,1]
	// so the spawn assembles a carrier core + three deployable payloads; the
	// sim spawn path honours it (see World.newCatalogCraft / splitNosePayloads).
	NosePayloadPlan []int
	// SlewRateDegPerSec (v0.10.0+) is the per-loadout attitude
	// angular-rate cap (deg/s, sim-time). Zero => the global
	// DefaultSlewRateDegPerSec. Loadout-level (not per-stage):
	// staging does not change the slew rate this cycle (attitude
	// dynamics are deferred). All catalog literals leave this unset
	// in v0.10.0; per-vehicle tuning is a follow-up dial.
	SlewRateDegPerSec float64

	// ScaleClass is the loadout's spawn-form scale hint (ADR 0014),
	// shared with bodies.System. Optional: an unset value normalizes
	// to bodies.ScaleReal via Scale(), so the existing real fleet needs
	// no per-literal change. The scale-matched Kern Stack sets
	// bodies.ScaleStrippedBack. Never used to filter craft by System —
	// any Loadout can be spawned in any System.
	ScaleClass bodies.ScaleClass

	// Category (v0.24 / ADR 0031) is the spawn-form display-grouping key the
	// CRAFT TYPE picker headers loadouts under (e.g. "launch-vehicles"). Copied
	// from LoadoutDef.Category; display-only, hash-free, distinct from Role.
	// Empty/unknown ⇒ the UI's trailing "Other" bucket.
	Category string
}

Loadout describes a named craft archetype — propulsion numbers, dry/wet mass sizing, default RCS pool, and visual differentiation (glyph + color). v0.8.2 ship set + v0.9.1 Saturn-V multi-stage + v0.9.4 SLS / Falcon 9:

  • S-IVB-1: J-2-powered third stage. The v0.5.13+ default.
  • ICPS: RL-10-powered low-TWR transfer stage. Returns from v0.5.6 — long burns, less mass.
  • RCS-tug: Pure-monoprop proximity-ops vehicle. No main engine; navigates entirely on RCS. For docking maneuvers.
  • Lander: Throttleable descent-stage profile (LM-derived). Lower thrust, lower Isp, sized for surface maneuvering.
  • Saturn-V: 3-stage Apollo launch vehicle (S-IC / S-II / S-IVB). v0.9.1+. TWR > 1 at sea level on stage 1.
  • SLS-Block1: 3-stage NASA heavy-lift (SRBs / Core / ICPS). v0.9.4+. SRBs and core fire in parallel in real life; we approximate as sequential.
  • Falcon-9: 2-stage SpaceX LV (Merlin 1D × 9 / Merlin Vacuum). v0.9.4+. Smaller stack, higher lift-off TWR.
  • Apollo-Stack: Saturn-V launch chain + LM + CSM payload, 5 stages. v0.10.1+. Mid-stage Lander decouples to a controllable craft (payload separation); CSM is the surviving core.

Future loadouts land alongside this catalog and are referenced from Spacecraft.LoadoutID — a string lookup keeps the on-disk format human-editable and lets future modding overlays add craft types without code changes.

v0.9.1+: Stages is the source of truth. Single-stage loadouts declare one entry; multi-stage (Saturn-V) declares the chain bottom-first (Stages[0] = S-IC booster, fires first; Stages[2] = S-IVB, fires last). The legacy flat fields (DryMass / Fuel / Isp / Thrust) are derived from Stages[0] for back-compat with pre- v0.9.1 readers — for single-stage loadouts they match the stage exactly; for multi-stage they reflect the bottom (firing) stage.

func LookupLoadout added in v0.8.2

func LookupLoadout(id string) Loadout

LookupLoadout returns the catalog entry for the given ID, or the S-IVB-1 default when the ID is empty / unknown. v0.8.2+: the fallback path keeps pre-v0.8.2 saves loadable — those entries have no LoadoutID and resolve to the default loadout.

func (Loadout) Crewed added in v0.24.0

func (l Loadout) Crewed() bool

Crewed reports whether the loadout flies with crew — true iff any stage declares a crewed command source (CommandCrewed), the same predicate SyncFields derives onto Spacecraft.Crewed (ADR 0027). The spawn-form CRAFT TYPE picker surfaces this as a crewed/uncrewed tag (ADR 0031 / S9). A command-less loadout whose Role is a crewed pod defaults to crewed only at construction (EnsureCommandSource); no shipped loadout relies on that, so this catalog-level predicate reads the explicit stage sources directly — the standalone Lander (probe-defaulted) correctly reads uncrewed.

func (Loadout) DryMass added in v0.8.2

func (l Loadout) DryMass() float64

DryMass returns the bottom stage's dry mass (single-stage equivalent for pre-v0.9.1 readers; sum-across-stages is via SumDryMass(l.Stages) when the caller wants it).

func (Loadout) Fuel added in v0.8.2

func (l Loadout) Fuel() float64

Fuel returns the bottom stage's fuel mass — same convention.

func (Loadout) Isp added in v0.8.2

func (l Loadout) Isp() float64

Isp returns the bottom stage's main-engine specific impulse.

func (Loadout) Scale added in v0.16.0

func (l Loadout) Scale() bodies.ScaleClass

Scale returns the loadout's normalized ScaleClass (empty => bodies.ScaleReal). The spawn form compares this against the target System's Scale() to surface the Δv-to-orbit / "best for" hint; it is never used to filter the craft list (ADR 0014).

func (Loadout) Thrust added in v0.8.2

func (l Loadout) Thrust() float64

Thrust returns the bottom stage's main-engine thrust.

type LoadoutDef added in v0.22.0

type LoadoutDef struct {
	ID                string    `json:"id"`
	Name              string    `json:"name,omitempty"`
	Role              string    `json:"role,omitempty"`
	Glyph             string    `json:"glyph,omitempty"`
	Color             string    `json:"color,omitempty"`
	Parts             []PartRef `json:"parts"`
	DecouplePlan      []int     `json:"decouple_plan,omitempty"`
	NosePayloadPlan   []int     `json:"nose_payload_plan,omitempty"`
	SlewRateDegPerSec float64   `json:"slew_rate_deg_per_sec,omitempty"`
	ScaleClass        string    `json:"scale_class,omitempty"`

	// Category (v0.24 / ADR 0031) is the spawn-form display grouping key
	// (e.g. "launch-vehicles", "satellites-payloads") — the CRAFT TYPE
	// picker headers loadouts under it. Display-only and NOT part of any
	// hash (the loadout catalog has no save-hash gate; catalog.go header),
	// distinct from the functionally-overloaded Role. An absent/unknown key
	// falls into a trailing "Other" bucket in the UI so a loadout never
	// vanishes; the key→label→order mapping is a fixed UI table in the
	// screens package, not data.
	Category string `json:"category,omitempty"`

	// Source is a runtime annotation ("embedded" / "user"), excluded from
	// JSON so it never affects round-trips or any future hash use.
	Source string `json:"-"`
}

LoadoutDef is a data-authored loadout: an ordered list of part references (bottom-first, the Stages convention) plus its plans and per-loadout tuning. The normalized counterpart of today's inline Loadout struct — a loadout references parts by ID rather than inlining full Stage literals (ADR 0026 §1).

func LoadCatalog added in v0.22.0

func LoadCatalog() (map[string]Part, []LoadoutDef, error)

LoadCatalog reads the embedded parts + loadouts catalog, merges any user overlay files, and returns the merged set. Warnings from malformed user files are dropped — call LoadCatalogWithWarnings to inspect them.

type ManeuverNode added in v0.8.1

type ManeuverNode struct {
	// ID (v0.16 / ADR 0016) is the node's stable identity, stamped by
	// World.stampNodeID when the node is planted. sortNodes reorders the
	// Nodes slice on every plant, so neither a slice index nor a pointer
	// survives an edit — a feature that must follow one specific node
	// (Auto-Warp's frozen target) resolves it by this ID instead
	// (World.nodeByID). Additive zero-value-omitempty, following the
	// TargetCraftID / PlaneChangeRad / BurnDirUnit precedent — nodes
	// planted before this field, or loaded from an older save, carry ID 0
	// until EnsureNodeIDs back-fills them, so no migration is needed.
	ID          uint64 `json:",omitempty"`
	TriggerTime time.Time
	Mode        BurnMode
	DV          float64
	Duration    time.Duration
	PrimaryID   string
	Event       TriggerEvent
	// Throttle (v0.7.6+) is the engine throttle setting [0, 1] used
	// for this node's burn. Zero (the JSON omitempty default) is
	// remapped to 1.0 — full open — by EffectiveThrottle so v1–v3
	// saves and pre-v0.7.6 plant paths keep their prior behaviour
	// without explicit migrations. Per-node throttle decouples
	// planted burns from live `Craft.Throttle` so adjusting throttle
	// mid-coast doesn't slow an in-flight planted burn.
	Throttle float64
	// TargetCraftID (v0.14.x / ADR 0012; was the one-based slate index
	// TargetCraftIdx) is the stable Spacecraft.ID of the target craft
	// this node was planted against, captured at plant time. Populated
	// only for the four target-relative modes (BurnTargetPrograde /
	// Retrograde / BurnTarget / AntiTarget) and for the
	// TriggerNextClosestApproach event. Zero-value-omitempty: non-target
	// nodes save without the field, no schema bump for that.
	//
	// Bound by identity at plant time so neither a later target switch
	// nor a slate mutation (end-flight / dock / undock / stage) silently
	// retargets the planted burn — the node stays aimed at the exact
	// craft the player chose, or degrades to no-op if that craft is gone
	// (resolve via World.craftByID at fire time). Zero means "no target".
	TargetCraftID uint64 `json:",omitempty"`
	// TargetGhostOwner (v0.28 S4, ADR 0034) names the remote player (ssh
	// key fingerprint) when this node was planted against a *ghost* —
	// another player's coasting craft. When non-empty, TargetCraftID is
	// the REMOTE craft's stable id, resolved against the transient ghost
	// slate (World.ghostByRef) rather than the local slate. Empty (the
	// common case) means TargetCraftID is a local craft id — unchanged
	// v0.14.x behaviour. A coasting ghost is Kepler-exact between reports,
	// so a node planted against it has craft-to-craft plan quality; when
	// the ghost burns the plan goes stale and the player replans — the
	// report corrects the ghost, not the node. NEVER persisted: save
	// drops the whole ref (owner + remote id) so a reloaded node can't
	// mistake a remote id for a local one. Zero-value-omitempty, no
	// schema bump.
	TargetGhostOwner string `json:",omitempty"`
	// PlaneChangeRad (v0.10.4+) is the signed orbital-plane rotation
	// angle (radians) for a BurnPlaneChange node — the angle the
	// horizontal velocity is rotated through about the radial axis.
	// Populated only for BurnPlaneChange (the `I` inclination auto-
	// plant); zero for every other mode. Zero-value-omitempty so
	// non-plane-change nodes save without the field — no schema bump,
	// same convention as TargetCraftIdx.
	PlaneChangeRad float64 `json:",omitempty"`
	// BurnDirUnit (v0.12.x+) is the fixed inertial (primary-relative)
	// unit thrust direction for a BurnVector node — the fused-Lambert
	// departure Δv direction, carrying eccentricity + raise + plane
	// change together. Populated only for BurnVector (the fused [H]
	// auto-plant); the zero vector for every other mode. Captured at
	// plant time and held for the burn (the craft slews to it). Save
	// round-trips it additively, following the CurrentAttitudeDir
	// schema-v6 precedent — no migration.
	BurnDirUnit orbital.Vec3 `json:",omitempty"`
	// AdvisoryKey (#293) tags a node planted by a single-keystroke
	// advisory planner — "K" (PlanRendezvousNudge) or "C"
	// (PlanCircularizeAtApoapsis) — with which key planted it. A second
	// press of the SAME key replaces its own previous unfired node
	// instead of stacking a stale duplicate behind it (the sim layer's
	// replaceAdvisoryNode matches on this field before planting); a
	// different advisory key, or an ordinary multi-step plan (H/I/
	// porkchop/manual), never touches another key's queued node. Empty
	// string (the zero-value omitempty default) means "not a
	// replaceable advisory node" — the common case for every node type
	// that predates this field. Additive zero-value-omitempty, same
	// precedent as ID / TargetCraftID / PlaneChangeRad / BurnDirUnit —
	// no save-schema migration needed.
	AdvisoryKey string `json:",omitempty"`
}

ManeuverNode represents a planned burn. v0.5.14+: TriggerTime is the burn-CENTER moment (the planner's intended firing point), not the burn start. For impulsive burns (Duration=0) center == start == TriggerTime. For finite burns the integrator actually starts the burn at TriggerTime - Duration/2 so the burn is centered on TriggerTime.

Duration controls finite vs impulsive: zero = instant Δv (legacy v0.1 path); non-zero = sustained engine burn lasting up to Duration or until DV is delivered, whichever first. Finite-burn execution is driven by Spacecraft.ActiveBurn during subsequent ticks.

PrimaryID is the body whose frame the burn was planned in (empty = the craft's home primary at plant time). Auto-plant transfers (v0.3.1) plant a geocentric departure plus a heliocentric / arrival- frame node; PrimaryID lets the planner UI render a frame-distinct glyph and lets the burn-execution layer warn if a node fires in an unexpected frame.

Event (v0.6.0+) selects whether TriggerTime is absolute or resolved from a live-orbit event. Zero value = TriggerAbsolute.

v0.8.1+: ManeuverNode lives on Spacecraft.Nodes (was World.Nodes).

func (ManeuverNode) BurnEnd added in v0.8.1

func (n ManeuverNode) BurnEnd() time.Time

BurnEnd returns the sim-time at which the integrator should terminate this node's burn (regardless of Δv-remaining or fuel state). v0.5.14+.

func (ManeuverNode) BurnStart added in v0.8.1

func (n ManeuverNode) BurnStart() time.Time

BurnStart returns the sim-time at which the integrator should fire this node's burn. For impulsive nodes (Duration=0) BurnStart equals TriggerTime. For finite nodes BurnStart is `TriggerTime - Duration/2` so the burn is centered on TriggerTime. v0.5.14+.

func (*ManeuverNode) ClearTargetCraftID added in v0.14.5

func (n *ManeuverNode) ClearTargetCraftID()

ClearTargetCraftID unbinds the node's target. v0.14.x.

func (*ManeuverNode) DropGhostRef added in v0.28.0

func (n *ManeuverNode) DropGhostRef()

DropGhostRef clears a ghost target ref (owner + the remote craft id). Called on save so a session-local ghost binding never persists and a remote id can't be reloaded as a local one — the burn geometry (mode / Δv / direction) is untouched. No-op for local-craft or untargeted nodes. v0.28 S4.

func (ManeuverNode) EffectiveThrottle added in v0.8.1

func (n ManeuverNode) EffectiveThrottle() float64

EffectiveThrottle returns the throttle to use when firing this node's burn, mapping the JSON omitempty zero-default to 1.0 (full open). v0.7.6+.

func (ManeuverNode) IsResolved added in v0.8.1

func (n ManeuverNode) IsResolved() bool

IsResolved reports whether the node's TriggerTime has been set — either because the node was planted with TriggerAbsolute or because the lazy-freeze resolver has fired for an event-relative node.

func (ManeuverNode) IsTargetRelative added in v0.9.3

func (n ManeuverNode) IsTargetRelative() bool

IsTargetRelative reports whether this node's burn mode requires a target craft state to resolve direction. v0.9.3+.

func (*ManeuverNode) SetTargetCraftID added in v0.14.5

func (n *ManeuverNode) SetTargetCraftID(id uint64)

SetTargetCraftID binds the node to a craft by its stable ID. v0.14.x.

func (ManeuverNode) TargetCraftIDValue added in v0.14.5

func (n ManeuverNode) TargetCraftIDValue() (uint64, bool)

TargetCraftIDValue returns the bound target craft's stable ID and ok=false when no target was bound at plant time. omitempty drops "no target" nodes from the wire. Resolve the ID to a live craft via World.craftByID at use time. v0.14.x / ADR 0012 (was TargetCraftIdxValue, which returned a slate index).

func (ManeuverNode) TargetGhostRef added in v0.28.0

func (n ManeuverNode) TargetGhostRef() (owner string, craftID uint64, ok bool)

TargetGhostRef returns the node's ghost target ref — the remote player's owner handle + craft id — and ok=false when the node isn't bound to a ghost. v0.28 S4 (ADR 0034).

type ManualBurn added in v0.8.1

type ManualBurn struct {
	StartTime time.Time
}

ManualBurn is the runtime state of a v0.7.3+ player-held manual burn. Mirrors ActiveBurn's role in the integrator dispatch but carries no Δv budget, no end time, and no fixed mode — direction comes from Spacecraft.AttitudeMode (which the player can update on the fly via the attitude keys), and the burn ends when the player stops it or fuel runs out. StartTime is informational only.

v0.8.1+: lives on Spacecraft.ManualBurn.

type Part added in v0.22.0

type Part struct {
	ID    string `json:"id"`
	Name  string `json:"name,omitempty"`
	Glyph string `json:"glyph,omitempty"`
	Color string `json:"color,omitempty"`
	// Tier is the configurator's one-word grouping hint ("booster",
	// "sustainer", "transfer", "payload", "tug") — purely descriptive,
	// carried over from StageModule.Tier. Not part of the runtime Stage.
	Tier string `json:"tier,omitempty"`

	// Physical numbers (units mirror Stage exactly).
	DryMassKg            float64 `json:"dry_mass_kg"`
	FuelMassKg           float64 `json:"fuel_mass_kg,omitempty"`
	FuelCapacityKg       float64 `json:"fuel_capacity_kg,omitempty"`
	ThrustN              float64 `json:"thrust_n,omitempty"`
	IspS                 float64 `json:"isp_s,omitempty"`
	MonopropMassKg       float64 `json:"monoprop_mass_kg,omitempty"`
	MonopropCapacityKg   float64 `json:"monoprop_capacity_kg,omitempty"`
	RCSThrustN           float64 `json:"rcs_thrust_n,omitempty"`
	RCSIspS              float64 `json:"rcs_isp_s,omitempty"`
	BallisticCoefficient float64 `json:"ballistic_coeff,omitempty"`

	// Launch-sprite + flame styling (ViewLaunch chase-cam silhouette).
	LaunchSpriteRowsPx  int    `json:"launch_sprite_rows_px,omitempty"`
	LaunchSpriteWidthPx int    `json:"launch_sprite_width_px,omitempty"`
	LaunchSpriteColor   string `json:"launch_sprite_color,omitempty"`
	LaunchSpriteHasLegs bool   `json:"launch_sprite_has_legs,omitempty"`
	FuelType            string `json:"fuel_type,omitempty"`

	// Capability flags (per-stage, mirror Stage).
	CanSoftLand  bool `json:"can_soft_land,omitempty"`
	HasParachute bool `json:"has_parachute,omitempty"`

	// Forward-compatible attributes consumed by cycle 2 (ADR 0027 — comms).
	// Declared in the schema now so it stays stable; C1 ignores them.
	// CommandSource is "crewed" | "probe" | "none" (empty == none);
	// Antenna declares the part's antenna {kind, power}.
	CommandSource string   `json:"command_source,omitempty"`
	Antenna       *Antenna `json:"antenna,omitempty"`

	// Components (v0.24 / ADR 0029 §1, the VAB cycle) optionally declares
	// this Part a composition of finer Components by ID. Absent ⇒ the Part
	// is atomic and its inline scalar fields above are authoritative
	// (unchanged, byte-identical). Present ⇒ the scalar fields are DERIVED
	// by aggregateComponents at load time (thrust-weighted Isp, additive
	// mass / capacity, single fuel type per stage). Zero migration: today's
	// catalog declares no components, so every existing Part stays atomic.
	Components []string `json:"components,omitempty"`

	// VabSeed (v0.25 / ADR 0032 §6) is an OPTIONAL component-ID list used
	// ONLY to seed the VAB working stage when the player cracks this atomic
	// part open (enter on its stage header). It NEVER drives runtime stats:
	// composePart / aggregateComponents read Components, never VabSeed, so the
	// part keeps its inline scalar fields authoritative and loadouts, budget
	// evals, and golden tests are unaffected by construction. The cracked
	// stage's aggregate may differ from the part — that delta is shown, not
	// hidden (§6). Hash-free and overlay-moddable like the rest of the
	// parts catalog (ADR 0026 §4).
	VabSeed []string `json:"vab_seed,omitempty"`
}

Part is a normalized, data-authored atomic stage — engine + tank + structure fused, exactly as Stages are atomic today. It is the single parts-catalog representation that unifies today's inline Loadout.Stages and the separate StageCatalog (ADR 0026 §1). One Part materializes into one runtime Stage via ToStage at spawn time. Stages stay atomic this cycle; the schema is left forward-compatible so a Part can later declare itself a composition of finer components (engine / tank / decoupler / antenna) at the VAB cycle (4).

func (Part) ToStage added in v0.22.0

func (p Part) ToStage() Stage

ToStage materializes a Part into a runtime Stage — a pure field copy. The part's own identity fields ride along (Name / Glyph / Color); the loadout-level LoadoutID is stamped by the loadout-assembly path (C1-3 NewFromLoadout), not here, since a part doesn't know which loadout references it. Tier (configurator metadata) has no Stage counterpart and is dropped. The comms attributes (command_source / antenna, ADR 0027) DO ride onto the Stage now (cycle 2 / C2-1).

type PartOverride added in v0.22.0

type PartOverride struct {
	FuelFillFraction *float64 `json:"fuel_fill_fraction,omitempty"`
	Name             string   `json:"name,omitempty"`
	Color            string   `json:"color,omitempty"`
}

PartOverride carries the only per-instance knobs a loadout may apply to a referenced part (ADR 0026 §1): a fuel fill fraction, a display name, and a color. Deliberately NOT arbitrary field overrides — that ambiguity is the rejected "hybrid" option. FuelFillFraction is a pointer so an absent override is distinguishable from an explicit 0.0 (empty tank).

type PartRef added in v0.22.0

type PartRef struct {
	PartID   string        `json:"part_id"`
	Override *PartOverride `json:"override,omitempty"`
}

PartRef is one entry in a loadout's ordered part list: a part ID plus an optional per-instance override.

type Spacecraft

type Spacecraft struct {
	// ID is the vessel's stable identity (v0.14.x / ADR 0012). Assigned
	// once from a monotonic World counter when the craft enters the
	// slate; never reused. Targets reference a craft by ID (not by its
	// slice position), so a slate mutation — end-flight, dock, undock,
	// stage — that shifts indices can no longer re-point a stored target
	// at the wrong vessel (GH #87). Zero means "unstamped"; the World
	// stamps it on spawn/load. Persisted (save schema v7+).
	ID uint64

	Name    string
	DryMass float64 // kg
	Fuel    float64 // kg
	Isp     float64 // s — specific impulse, used by finite burns
	Thrust  float64 // N — max engine thrust; zero disables finite burns

	// LoadoutID, Role, Glyph, Color (v0.8.2+) are the craft-type
	// axes from the v0.8 plan §scoping #3:
	//   (i)   propulsion loadout — references Loadouts[ID]; the
	//         per-craft Thrust / Isp / DryMass / Fuel are populated
	//         from the loadout at construction time.
	//   (ii)  role — free-form tag (transfer-stage / lander /
	//         orbiter / tug). HUD/mission-predicate facing only;
	//         no physics impact in v0.8.
	//   (iii) visual — Glyph + Color override the canvas marker so
	//         each craft reads distinctly even when zoomed out
	//         beyond the chevron-resolving threshold.
	//
	// All four are zero-default-safe: pre-v0.8.2 saves load with
	// empty strings, and the lookup paths fall back to the S-IVB-1
	// default loadout when LoadoutID is empty.
	LoadoutID string
	Role      string
	Glyph     string
	Color     string

	// Throttle is the engine power factor in [0, 1]; effective
	// thrust = Thrust * Throttle. v0.7.3+. Zero means "engine off"
	// (the live value after the player cuts throttle) — it is NOT a
	// legacy/unset sentinel promoted to 1.0; Spacecraft.EffectiveThrottle
	// returns it verbatim. Every constructor must therefore set Throttle
	// explicitly (NewInLEO and the save-load path do); literal
	// Spacecraft{} test fixtures use Thrust=0 so the engine path is
	// never entered and the value is moot. (ManeuverNode.EffectiveThrottle
	// is the one that maps 0→1.0, for the per-node firing throttle —
	// that promotion is node-local and does not apply here.)
	Throttle float64

	// LastThrottleChangeAt is the sim-time at which Throttle most
	// recently changed value. v0.8.6.x+: the warp clamp uses this
	// to suppress high warp for a brief window after the player
	// adjusts throttle, so a 1000× throttle ramp doesn't alias the
	// integrator the same way a finite burn does. Zero value means
	// "never changed since spawn" — treated as no recent change.
	// Not persisted to saves; resets on load (acceptable since the
	// clamp window is sub-second).
	LastThrottleChangeAt time.Time `json:"-"`

	// v0.8.0 — RCS / monopropellant precision-maneuver thruster.
	// Monoprop is the consumable propellant pool (kg); MonopropCapacity
	// is the max tank load. RCSThrust is total RCS engine thrust (N),
	// sized linearly off DryMass at construction. RCSIsp is the
	// specific impulse of the monoprop engine (~220 s, vs ~420 s for
	// the J-2 main).
	//
	// All four are zero on legacy v3 saves; the loader populates
	// defaults from DryMass so old saves inherit RCS without a
	// schema bump.
	Monoprop         float64
	MonopropCapacity float64
	RCSThrust        float64
	RCSIsp           float64

	// v0.8.4 — atmospheric drag coupling. BallisticCoefficient is
	// (C_D · A / m) in m²/kg — the multiplicative factor in the drag
	// equation a = -0.5 · ρ · |v_rel|² · BC · v̂_rel. Higher means
	// more drag per unit dynamic pressure (the inverse of the
	// aerospace-standard m/(C_D·A) convention; named for what the
	// integrator actually multiplies). Zero is treated as the default
	// 0.01 m²/kg (S-IVB-1 baseline) so legacy saves don't need a
	// schema bump — see EffectiveBallisticCoefficient.
	BallisticCoefficient float64

	Primary bodies.CelestialBody
	State   physics.StateVector

	// SystemIdx (v0.16 / ADR 0015) binds this Vessel to one System for
	// its lifetime, fixed at spawn. It is an index into the
	// name-sorted-Sol-first w.Systems slice. The simulator integrates
	// each Vessel against w.Systems[SystemIdx] — not the currently-viewed
	// system — so a parked Sol craft keeps orbiting correctly while the
	// player flies a craft in another System. There is no interstellar
	// transfer; SOI transitions and Docking stay within one System. Zero
	// (Sol) is the correct default for the seed Vessel and any save
	// predating the per-Vessel binding (see save_migrate_v7_to_v8).
	SystemIdx int

	// v0.8.1+ — per-craft mission/flight state. Pre-v0.8.1 these
	// lived on World, which meant a single planted burn was shared
	// across all craft and the in-flight ActiveBurn followed
	// whichever craft was active at integrator time. Per-craft
	// ownership ties planted nodes + live engine state to the craft
	// they were planted for, regardless of which craft the player
	// is currently flying.
	//
	// Nodes are sorted by TriggerTime ascending (sim package owns
	// the sort helper). ActiveBurn / ManualBurn are mutually
	// exclusive — a planted finite burn or a held manual burn, not
	// both. AttitudeMode + EngineMode are the live manual-flight
	// state.
	Nodes        []ManeuverNode
	ActiveBurn   *ActiveBurn
	ManualBurn   *ManualBurn
	AttitudeMode BurnMode
	EngineMode   EngineMode

	// RCSFineLevel selects the per-pulse Δv magnitude for live RCS
	// trim: level 0 = the coarse RCSDvQuantum (0.1 m/s), each level up
	// divides by 10 (0.01, 0.001 m/s) for sub-second orbital-period
	// trim on a low-budget comsat. Transient manual-flight state — not
	// persisted (resets to coarse on load) and dropped on dock,
	// alongside AttitudeMode / EngineMode.
	RCSFineLevel int

	// CurrentAttitudeDir (v0.10.0+) is the craft's *actual* nose
	// unit vector in the same world/primary frame as State.R/V —
	// the physical orientation, distinct from the *commanded*
	// direction recomputed from AttitudeMode each tick. The slew
	// integrator (sim.integrateOneCraft) rotates this toward the
	// commanded direction at SlewRate; stepThrust + the navball
	// sub-observer read it instead of recomputing, so burning
	// before alignment bleeds Δv to cosine loss. A zero vector
	// means "uninitialized" — the first slew tick snaps it to the
	// commanded direction (no slew-from-garbage, no nose teleport
	// on a pre-v0.10.0 save). Persists in saves so a craft caught
	// mid-slew restores its real nose.
	CurrentAttitudeDir orbital.Vec3

	// SlewRateDegPerSec (v0.10.0+) caps attitude angular rate in
	// **sim-time** (deg/s, integrated against the warp-scaled tick).
	// Zero => DefaultSlewRateDegPerSec. Set from the loadout at
	// construction (NewFromLoadout); not stage-derived, so SyncFields
	// does not touch it, and it is re-applied via the loadout on load
	// rather than persisted.
	SlewRateDegPerSec float64

	// DockedComponents (v0.8.3+) records the original craft that
	// fused into this composite, so an Undock keystroke can
	// restore them. Empty for non-composite craft. Populated by
	// sim.DockCrafts in render-order; flattened across chained
	// docks (a composite that docks with another contributes both
	// its own components and the other's identity to the result).
	DockedComponents []DockedComponent

	// Landed (v0.9.2+): true when the craft is parked on the
	// primary's surface co-rotating with the ground. While Landed,
	// the integrator bypasses gravity / drag / thrust and recomputes
	// R from `LaunchLatDeg` / `LaunchLonDeg` each tick using the
	// renderer's `BodyFixedToWorld` projection — so the craft stays
	// at the texture-rendered "Cape Canaveral" pixel as the body
	// rotates. Cleared automatically when the engine ignites — see
	// `World.StartManualBurn` and the planted-burn fire path. Set
	// on `SpawnSpec.Launchpad=true` spawns. Persists in saves so a
	// paused-on-pad session restores correctly.
	Landed bool

	// LaunchLatDeg / LaunchLonDeg (v0.9.2+) record the body-fixed
	// (lat, lon) of the launchpad spawn. Only meaningful when
	// Landed=true; the integrator re-derives R from these +
	// the body's current rotation phase each tick (rather than
	// rotating R via Rodrigues, which drifted off the texture's
	// Florida pixel because the v0.8.5+ Snyder-orthographic
	// rendering has a sub-observer-frame rotation that's view-
	// dependent — see render.BodyFixedToWorld doc).
	//
	// Latitude in degrees north positive; longitude in degrees east
	// positive (real-Earth-style). Persists in saves.
	LaunchLatDeg float64
	LaunchLonDeg float64

	// Crashed (v0.11.4+): destructive-impact lifecycle flag. Set by
	// the surface-contact predicate (physics.ClampToSurface call
	// site) when impact velocity exceeds V_CRIT or nose alignment
	// fails NOSE_TOL, or when the vessel is not designed to
	// soft-land. While Crashed the vessel skips integration (no
	// gravity / drag / thrust / slew) and renders dimmed with no
	// flame. Cleared only by end-flight removal (vessel leaves the
	// world). Persists in saves so a paused-mid-impact session
	// restores the crashed state. See ADR 0004 for the full
	// lifecycle. `omitempty`-default-false; no SchemaVersion bump.
	Crashed bool

	// CanSoftLand (v0.11.4+): true when the vessel kind is designed
	// to land — Apollo-LM-style Lander, Falcon-9 first stage. The
	// surface-contact predicate consults this as a hard prerequisite
	// for the soft-land branch: a Saturn V capsule that grazes the
	// surface at 5 m/s is Crashed, not Landed, even though the
	// kinematic checks would otherwise qualify. Sourced from the
	// catalog loadout at construction; not mutated at runtime.
	// `omitempty`-default-false (existing vessels are crash-only).
	CanSoftLand bool

	// HasParachute (v0.12 Slice 3, ADR 0008): the Vessel-level mirror
	// of the bottom stage's per-Stage parachute capability. Re-derived
	// from Stages[0] on every SyncFields exactly like CanSoftLand, so
	// it rides the hardware across a decouple (the chute capability
	// becomes "active" once the chute-bearing stage is the bottom /
	// surviving core). Gates the Stage-action arm path and the
	// auto-deploy check. `omitempty`-default-false.
	HasParachute bool

	// Crewed / Controllable (v0.23 / ADR 0027): vessel-level mirrors of
	// the per-stage CommandSource, re-derived by SyncFields across the
	// whole stack on every staging / dock / load. Controllable is true
	// when the vessel has any command source (crewed pod or probe core);
	// Crewed is true when any is a crewed pod (crewed vessels are never
	// comms-gated). A vessel with neither is passive debris. Construction
	// (NewFromLoadout / NewFromStages) and save-load stamp a default
	// command source on a command-less *vessel* so it stays controllable;
	// jettisoned stages get no default, so a spent booster is debris.
	Crewed       bool
	Controllable bool

	// AntennaKind / AntennaRangeM (v0.23 / ADR 0027): the vessel's
	// effective comms antenna — the longest-ranged one across its stages,
	// re-derived by SyncFields. Read by the connectivity graph. AntennaNone /
	// zero means no antenna. AntennaRangeM is a rated range in metres (the
	// combinability model; see sim.commLinkRangeM).
	AntennaKind   string
	AntennaRangeM float64

	// ChuteState (v0.12 Slice 3, ADR 0008): the runtime parachute
	// deploy state — STOWED → ARMED → DEPLOYED, one-way, DEPLOYED
	// terminal. Lives alongside Landed / Crashed (the other surface-
	// lifecycle runtime flags). Zero value = ChuteStowed, so pre-Slice-3
	// saves load stowed; `omitempty`, no SchemaVersion bump. While
	// ChuteDeployed, EffectiveBallisticCoefficient returns the fixed
	// ChuteDeployedBC and the surface-arrival predicate gains a second
	// (nose-waived) route into Landed.
	ChuteState ChuteState

	// OnPad (v0.11.4+): true between Launchpad spawn and first
	// liftoff. Set by surfaceSpawnPosVel; cleared on the first
	// Landed=false transition. Distinguishes "fresh launchpad
	// spawn" from "post-flight soft land" for the ViewLaunch
	// auto-route handler (which fires only when OnPad && Landed
	// transitions false→true). Soft-lands clear OnPad on liftoff,
	// so the post-flight Landed transition does NOT rip the player
	// into ViewLaunch mid-touchdown.
	OnPad bool

	// LandedLatDeg / LandedLonDeg (v0.11.4+): soft-landed touchdown
	// coordinates. When non-zero, integrateLanded reads these instead
	// of LaunchLatDeg / LaunchLonDeg (which retain their original
	// spawn-site meaning — useful for downrange-from-launch reads
	// even after a return-and-relaunch cycle). Same north-positive /
	// east-positive convention as LaunchLatDeg.
	LandedLatDeg float64
	LandedLonDeg float64

	// PitchTrim (v0.9.2+) is a signed pitch offset (radians)
	// applied on top of the active BurnMode's computed direction.
	// Positive values rotate the thrust vector eastward of the
	// mode's natural direction (about the local-north axis at the
	// craft's current position); negative rotates west. Used by
	// ascent gravity-turn flight: the player launches BurnRadialOut
	// (vertical), trims +5–15° east via the `<` / `>` keys to start
	// the gravity turn, then switches to BurnSurfacePrograde once
	// surface-relative velocity is established. Reset via the `\`
	// key. Persists in saves so a paused-mid-ascent session restores
	// the player's trim setting.
	PitchTrim float64

	// Stages (v0.9.1+) is the source of truth for dry mass /
	// propellant / engine numbers. Stages[0] is the BOTTOM stage
	// (the currently-firing engine + the next to be jettisoned by
	// World.StageActive); Stages[len-1] is the TOP stage (core
	// payload — last to fire). Single-stage craft carry exactly
	// one Stage.
	//
	// The historical flat fields above (DryMass, Fuel, Thrust,
	// Isp, Monoprop, MonopropCapacity, RCSThrust, RCSIsp) are
	// derived shadow-mirror values refreshed by SyncFields
	// (stage.go). Read sites use the flat fields directly — no
	// API churn for the dozens of pre-v0.9.1 consumers. Write
	// sites must mutate the relevant Stage entry and call
	// SyncFields (or use the BurnFuel / BurnMonoprop helpers).
	//
	// Save schema v6 serializes Stages; the flat fields are
	// re-derived on Load via SyncFields. Pre-v6 saves migrate by
	// wrapping the v5 flat fields into a single-element Stages
	// slice (see internal/save/save_migrate_v5_to_v6.go).
	Stages []Stage

	// DecouplePlan (v0.12 Slice 2 / ADR 0007) is a bottom-up list of
	// group sizes describing how many contiguous bottom Stages each
	// staging press releases as a single jettisoned craft. Nil/empty
	// ⇒ all-ones (one Stage per press — the historical behaviour).
	// Copied from the Loadout at construction (NewFromLoadout) and
	// consumed positionally by World.StageActive: each press pops
	// DecouplePlan[0] bottom stages, then advances DecouplePlan =
	// DecouplePlan[1:]. The Apollo Stack ships [1,1,1,2] so the
	// descent + ascent LM pair extracts together as one 2-stage
	// craft, leaving the CSM core. A released multi-stage craft
	// inherits NO plan, so its internal boundaries fall back to
	// single-pop (the extracted LM surface-stages its descent alone
	// with no special-casing). Persisted on the save wire as
	// decouple_plan,omitempty so a mission saved mid-staging restores
	// the correct remaining grouping.
	DecouplePlan []int

	// Target (v0.9.3 polish) is this craft's bound target. Pre-
	// polish, target was a single World.Target slot shared across
	// all crafts; pressing `T` while controlling craft A would
	// toggle the target visible to craft B too. Per-craft Target
	// gives each vessel its own binding, restored on switch via
	// World.setActiveCraftIdx so w.Target stays in sync with the
	// currently-active craft. Zero value (TargetNone) is the safe
	// default for fresh / loaded crafts.
	Target Target
}

Spacecraft is the player vessel. Mass split: DryMass is the bus, Fuel is consumable. State is relative to Primary.

func NewFromLoadout added in v0.8.2

func NewFromLoadout(loadoutID string) *Spacecraft

NewFromLoadout constructs a Spacecraft from a loadout entry. The caller still has to set Primary + State (orbit), name, and any per-instance overrides. Convenience for spawn paths so they don't duplicate the field-setting boilerplate.

v0.9.1+: Stages is the source of truth — copied from the catalog entry — and SyncFields populates the legacy flat fields (DryMass / Fuel / Thrust / Isp / Monoprop / RCSThrust / RCSIsp) from Stages so pre-v0.9.1 readers keep working without per-site changes.

func NewFromLoadoutValue added in v0.24.0

func NewFromLoadoutValue(l Loadout) *Spacecraft

NewFromLoadoutValue constructs a Spacecraft from a resolved Loadout VALUE (not a catalog ID). The spawn path for saved VAB designs uses this: a design resolves to a Loadout that lives OUTSIDE the global Loadouts map (ADR 0029 §4 — designs are a separate namespace), so it can't go through the ID-keyed NewFromLoadout. Identical otherwise — copies Stages + DecouplePlan, defaults the command source, and syncs — so a design-spawned craft is byte-identical to the equivalent catalog craft.

func NewFromStages added in v0.10.1

func NewFromStages(stages []Stage) *Spacecraft

NewFromStages constructs a Spacecraft from a player-assembled stage list (bottom-first, same convention as Loadout.Stages) — the v0.10.1+ stack-configurator path. Sibling of NewFromLoadout with no catalog entry behind it: LoadoutID is left empty (a custom craft is not a catalog archetype), and identity/visuals come from the top (core) stage so the slate HUD has a sensible name + marker for the vessel the player keeps flying.

The caller still sets Primary + State. Returns nil when stages is empty — an empty stack is not a spawnable craft (callers reject before reaching the spawn path).

Custom craft persist through save/load via the existing v6 per-stage wire format (save schema v6, v0.9.1) — no migration: the flat shadow fields are re-derived by SyncFields on load and the empty LoadoutID resolves to the default only for those derived mirrors, never overriding the round-tripped Stages.

func NewInLEO

func NewInLEO(earth bodies.CelestialBody) *Spacecraft

NewInLEO builds a spacecraft in a 500 km circular prograde parking orbit around the provided primary (typically Earth). Orbit lies in the primary's equatorial plane (z=0) with periapsis along +X, velocity along +Y. v0.6.1: bumped from 200 → 500 km — clears the visual zone close to the Earth disk so the live orbit ellipse and craft glyph are immediately distinguishable from the body when the camera spawns focused on the craft.

Mass / propulsion numbers (v0.5.13+) modeled on the Saturn V S-IVB — the J-2-powered third stage that performed trans-lunar injection for every Apollo Moon mission:

  • DryMass 11000 kg (S-IVB empty was ~12 400 kg; rounded down for a no-payload solo profile)
  • Fuel 40000 kg (much less than real S-IVB's ~106 t — sized for Δv 6.3 km/s, enough for Luna round trip without over-provisioning)
  • Isp 421 s (J-2 vacuum)
  • Thrust 1023 kN (J-2 spec)

Δv budget = 421 × g₀ × ln(51000/11000) ≈ 6.3 km/s — Luna round trip with margin. TLI burn time at this thrust ≈ 110 s (vs ~10 min for the pre-v0.5.13 ICPS). The short burn keeps gravity-rotation finite-burn loss < 0.1%, so the auto-plant Hohmann delivers near-exact apoapsis without needing the impulsive workaround. Pre-v0.5.13 the ICPS-class vessel had a 10-min TLI that lost ~27% of apoapsis-raise to integration error.

History: v0.5.6 ICPS-1, v0.5.13+ S-IVB-1. Apollo's actual TLI stage is a better fit for the no-payload Luna-mission profile this default targets.

func NewInLEOAtPhase added in v0.32.2

func NewInLEOAtPhase(earth bodies.CelestialBody, phaseDeg float64) *Spacecraft

NewInLEOAtPhase is NewInLEO generalised over the position on the orbit: the same default S-IVB-1 on the same 500 km circular equatorial orbit, placed phaseDeg degrees prograde around the ring from the seed spot (body-frame +X). NewInLEO is exactly phase 0. The multiplayer fleet reset (--reset-fleet) uses this to space each enrolled player's fresh vessel evenly around one shared orbit.

func (*Spacecraft) ActiveStageFuel added in v0.9.4

func (s *Spacecraft) ActiveStageFuel() float64

ActiveStageFuel returns the bottom (currently-firing) stage's main-engine propellant in kg. Used by the engine-cutoff path to decide whether the active engine still has fuel.

v0.9.4+: replaces direct checks against s.Fuel (which is the SUMMED propellant across all stages). For a 3-stage Saturn V with a dry S-IC and full S-II + S-IVB, s.Fuel reads ~549,000 kg even though the firing engine has nothing to burn — the engine kept thrusting "for free" until the player staged.

Falls back to s.Fuel when Stages is empty (legacy / test fixtures constructed without Stages); single-stage craft are unaffected since the sum equals the bottom stage's fuel.

func (*Spacecraft) Altitude

func (s *Spacecraft) Altitude() float64

Altitude returns |r| − primary mean radius. Can go negative if the spacecraft is inside the primary — the caller (HUD / crash detection) decides what to do about that.

func (*Spacecraft) ApplyImpulsive

func (s *Spacecraft) ApplyImpulsive(mode BurnMode, dv float64)

ApplyImpulsive adds a delta-v of magnitude dv m/s in the given direction mode, instantly. Fuel is deducted using the rocket equation as a proxy (Isp·g·ln(m0/m1) for the actually-burned Δv); in v0.1 we approximate with a linear consumption rate — plan §MVP defers true rocket-eq to v0.2.

Target-relative modes degrade to no-op without a target snapshot; callers with a target use ApplyImpulsiveWithTarget. v0.9.3+.

func (*Spacecraft) ApplyImpulsiveDir added in v0.10.4

func (s *Spacecraft) ApplyImpulsiveDir(dir orbital.Vec3, dv float64)

ApplyImpulsiveDir applies an instantaneous Δv of magnitude dv along the supplied unit direction, debiting fuel via the rocket equation. Used by the planted-node fire path for a BurnPlaneChange node that degraded to impulsive — its tilted direction is resolved upstream by NodeBurnDirection rather than from a plain BurnMode. v0.10.4+.

func (*Spacecraft) ApplyImpulsiveWithTarget added in v0.9.3

func (s *Spacecraft) ApplyImpulsiveWithTarget(mode BurnMode, dv float64, rT, vT orbital.Vec3)

ApplyImpulsiveWithTarget is ApplyImpulsive with a target snapshot in the same frame as Spacecraft.State. Used by the planted-node fire path (sim/maneuver.go) for target-relative impulsive nodes. v0.9.3+.

func (*Spacecraft) ApplyRCSPulse added in v0.8.0

func (s *Spacecraft) ApplyRCSPulse(mode BurnMode) bool

ApplyRCSPulse delivers one RCSDvQuantum of Δv in the given burn direction, debiting monoprop via the rocket equation against the RCSIsp engine. No-op if monoprop is empty or RCSThrust / RCSIsp are unconfigured (legacy save with zero RCS fields, mid-load before the loader populates defaults). v0.8.0+.

Target-relative modes degrade to no-op (zero direction) without a resolved target snapshot — callers with a target use ApplyRCSPulseWithTarget. v0.9.3+.

func (*Spacecraft) ApplyRCSPulseWithTarget added in v0.9.3

func (s *Spacecraft) ApplyRCSPulseWithTarget(mode BurnMode, rT, vT orbital.Vec3) bool

ApplyRCSPulseWithTarget is ApplyRCSPulse with a resolved target snapshot in the same frame as Spacecraft.State (primary-relative when both share a primary, fully inertial otherwise — caller resolves via World.targetStateRelativeToActivePrimary). The four target-relative modes use the snapshot to compute direction; other modes ignore it. v0.9.3+.

func (*Spacecraft) ArmParachute added in v0.12.4

func (s *Spacecraft) ArmParachute() bool

ArmParachute moves a stowed parachute to the armed state. Returns true if it transitioned. It is a no-op (returns false) when the craft lacks the capability (HasParachute false), when the chute is already armed, or when it is already deployed (terminal). Arming is allowed in any conditions, including vacuum — "arm on the way down and forget it"; auto-deploy fires later when dynamic pressure reaches the floor (ADR 0008 §2). Callers gate the player-facing trigger on the chute being the bare top stage (the Stage action's single-stage no-op branch).

func (*Spacecraft) AsDockedComponent added in v0.8.3

func (s *Spacecraft) AsDockedComponent() DockedComponent

AsDockedComponent captures s's identity + capacity fields into a DockedComponent record. v0.8.3+: used by DockCrafts to populate the composite's DockedComponents list.

func (*Spacecraft) BurnDirection added in v0.9.2

func (s *Spacecraft) BurnDirection(mode BurnMode) orbital.Vec3

BurnDirection returns the unit thrust direction for the active craft given a burn mode, taking into account:

  • Surface-relative modes (BurnSurfacePrograde / Retrograde) which need the craft's primary spin axis (ω) to compute the surface-relative velocity v - ω × r. Pre-launch (zero surface velocity) returns the zero vector; the caller interprets that as "no defined direction" — the burn is a no-op until the craft is moving relative to the ground.
  • PitchTrim (v0.9.2+) — a player-set ± rotation about the local-north axis applied on top of the mode's natural direction, for ascent gravity-turn manual flight.

Live-craft call sites (RCS pulse, manual burn, ActiveBurn fire) use this method instead of the bare DirectionUnit so surface modes + trim feed through. Predictor / speculative call sites without a *Spacecraft in scope keep using DirectionUnit; surface modes there return zero (degraded — predictor doesn't simulate future v_surface).

v0.9.2+. v0.9.3+: target-relative modes added; this wrapper passes zero target state, so the four target modes degrade to no-op here — callers with a target use BurnDirectionWithTarget.

func (*Spacecraft) BurnDirectionForBurn added in v0.12.1

func (s *Spacecraft) BurnDirectionForBurn(mode BurnMode, rT, vT orbital.Vec3, planeRad float64, burnDir orbital.Vec3) orbital.Vec3

BurnDirectionForBurn resolves a planted / in-flight burn's unit thrust direction, additionally handling BurnVector (v0.12.x+) via the captured fixed-inertial burnDir — a direction a BurnMode alone can't decode, just like BurnPlaneChange's planeRad. BurnVector ignores (rT, vT, planeRad) and craft state; every other mode delegates to BurnDirectionPlaneAware. The firing/slew path (sim) uses this wrapper because the captured vector rides on the ManeuverNode / ActiveBurn.

v0.12.x+.

func (*Spacecraft) BurnDirectionPlaneAware added in v0.10.4

func (s *Spacecraft) BurnDirectionPlaneAware(mode BurnMode, rT, vT orbital.Vec3, planeRad float64) orbital.Vec3

BurnDirectionPlaneAware resolves a burn direction like BurnDirectionWithTarget, additionally handling BurnPlaneChange via the supplied signed plane-change angle (radians). planeRad is ignored for every other mode. The planted-node and active-burn paths use this wrapper because the rotation angle rides on the ManeuverNode / ActiveBurn — a BurnMode alone can't decode it.

v0.10.4+.

func (*Spacecraft) BurnDirectionWithTarget added in v0.9.3

func (s *Spacecraft) BurnDirectionWithTarget(mode BurnMode, rT, vT orbital.Vec3) orbital.Vec3

BurnDirectionWithTarget is BurnDirection with a target snapshot in the same frame as Spacecraft.State (primary-relative when both share a primary, fully inertial otherwise — caller resolves the frame via World.targetStateRelativeToActivePrimary).

The four target-relative modes (BurnTargetPrograde / Retrograde / BurnTarget / AntiTarget) consume (rT, vT); other modes ignore it and behave identically to BurnDirection.

v0.9.3+.

func (*Spacecraft) BurnFuel added in v0.9.1

func (s *Spacecraft) BurnFuel(amount float64) float64

BurnFuel decrements the bottom-stage main-engine fuel by amount (kg), clamped to [0, Stages[0].FuelMass]. Refreshes the flat shadow fields. v0.9.1+ replacement for the pre-staging pattern `c.Fuel -= amount`. Returns the amount actually burned (clamped).

func (*Spacecraft) BurnMonoprop added in v0.9.1

func (s *Spacecraft) BurnMonoprop(amount float64) float64

BurnMonoprop decrements the bottom-stage RCS propellant by amount (kg), clamped to [0, Stages[0].MonopropMass]. Refreshes the flat shadow fields. v0.9.1+ replacement for `s.Monoprop -= amount`. Returns the amount actually burned (clamped).

func (*Spacecraft) BurnStalled added in v0.12.6

func (s *Spacecraft) BurnStalled() bool

BurnStalled reports whether a planted burn is paused waiting for the player to stage: Δv is still owed but the firing (bottom) stage has run dry, so no thrust is being produced. The burn stays alive and resumes automatically once a fuelled stage is decoupled into place (v0.12.x pause-and-resume across staging). Drives the HUD "stage to resume" prompt and the throttle-cut abort path.

func (*Spacecraft) BurnTimeForDV added in v0.6.5

func (s *Spacecraft) BurnTimeForDV(dv float64) time.Duration

BurnTimeForDV returns the engine-on duration required to deliver dv at the craft's current mass + thrust + Isp, using the rocket-equation form t = (m0/ṁ)·(1 − exp(−Δv/(Isp·g0))). Accounts for the mass loss during the burn — at high Δv-fraction of the budget, a constant-mass approximation underestimates the time by the integral of mass / thrust, which matters for low-TWR vessels burning a large share of their fuel.

Returns 0 when no finite burn is possible: zero or non-positive Δv, no thrust, no Isp, or Δv exceeding what the available fuel can support (caller's exceeds-budget warning fires; the integrator caps delivery at fuel exhaustion regardless of the duration the form committed). v0.6.5+: replaces the prior UI-set duration field; the planner now derives this so the player only specifies Δv.

func (*Spacecraft) EffectiveBallisticCoefficient added in v0.8.4

func (s *Spacecraft) EffectiveBallisticCoefficient() float64

EffectiveBallisticCoefficient returns the per-craft drag coefficient (C_D · A / m, m²/kg). v0.9.2.1+: prefers the bottom stage's per-stage BC (Stages[0].BallisticCoefficient) so a multi-stage craft gets the firing stage's actual cross-section / mass profile — critical for low-altitude Saturn V launches where the v0.8.4 default (0.01 m²/kg, tuned for an LEO S-IVB-1 where drag was always zero) makes drag dominate at sea level. Falls back to s.BallisticCoefficient (legacy field), then DefaultBallisticCoefficient.

func (*Spacecraft) EffectiveThrottle added in v0.7.3

func (s *Spacecraft) EffectiveThrottle() float64

EffectiveThrottle returns Throttle clamped to [0, 1]. Zero means "engine off" — that's the real value the player sees in the HUD after pressing `x` (cut throttle), so it cannot be silently promoted to 1.0. All Spacecraft constructors must set Throttle explicitly (see NewInLEO and the save-load path); the test fixtures that build literal Spacecraft{} use Thrust=0 so the engine path is never entered and the throttle value is moot.

func (*Spacecraft) MassFlowRate added in v0.2.1

func (s *Spacecraft) MassFlowRate() float64

MassFlowRate returns the propellant mass-flow magnitude (kg/s) at the spacecraft's *live* throttle. The integrator's manual-burn path uses this; the planted-burn path uses MassFlowRateAt with the captured ActiveBurn.Throttle so adjusting the live throttle knob mid-coast doesn't slow a planted burn (v0.7.6+).

func (*Spacecraft) MassFlowRateAt added in v0.7.6

func (s *Spacecraft) MassFlowRateAt(throttle float64) float64

MassFlowRateAt returns the propellant mass-flow at an explicit throttle setting, clamped to [0, 1]. Used by the active-burn integrator path to honour the per-node throttle captured at burn-start. v0.7.6+.

func (*Spacecraft) OrbitalSpeed

func (s *Spacecraft) OrbitalSpeed() float64

OrbitalSpeed returns |v| in the primary-relative frame.

func (*Spacecraft) RCSDeltaV added in v0.8.0

func (s *Spacecraft) RCSDeltaV() float64

RCSDeltaV estimates how much more Δv the current monoprop pool supports via the rocket equation against TotalMass minus monoprop (i.e. the dry-fuel mass after the monoprop is exhausted). v0.8.0+.

func (*Spacecraft) RCSPulseDV added in v0.24.5

func (s *Spacecraft) RCSPulseDV() float64

RCSPulseDV returns the per-pulse Δv for the craft's current RCS fine level: RCSDvQuantum (0.1 m/s) at level 0, divided by 10 each level up (0.01 m/s, 0.001 m/s) for sub-second orbital-period trim. A level outside [0, RCSFineLevels) falls back to the coarse default. v0.24.5+.

func (*Spacecraft) RemainingDeltaV

func (s *Spacecraft) RemainingDeltaV() float64

RemainingDeltaV estimates how much more Δv the main engine's fuel supports via the rocket equation: Δv = Isp·g0·ln(m0/m_after_fuel). Only the active (bottom) stage's propellant is burnable through the firing engine, so m_after_fuel subtracts ActiveStageFuel() from the stacked total — everything else (dry mass, monoprop, *upper-stage propellant*) is dead weight to this engine.

v0.14.x (GH #89): the floor was DryMass+Monoprop, but s.Fuel / DryMass are the SUMMED values across all stages, so the implied m_after_fuel counted every upper stage's propellant as burnable through the bottom engine — the same all-stage overestimate ActiveStageFuel() was introduced to kill. For a single-stage craft TotalMass()-ActiveStageFuel() == DryMass+Monoprop, so behaviour is unchanged there; only multi-stage budgets are corrected.

func (*Spacecraft) SlewRateRad added in v0.10.0

func (s *Spacecraft) SlewRateRad() float64

SlewRateRad returns the craft's attitude slew-rate cap in rad/s, falling back to DefaultSlewRateDegPerSec when the per-craft value is unset (zero). This is why legacy saves and bare Spacecraft{} test fixtures get a sane rate without a persisted field — the rate is a loadout/catalog property, re-applied on construction.

func (*Spacecraft) SlewToward added in v0.10.0

func (s *Spacecraft) SlewToward(commanded orbital.Vec3, dt float64)

SlewToward rotates CurrentAttitudeDir toward the commanded unit direction by at most SlewRate·dt radians, about their mutual perpendicular. dt is the warp-scaled sim-time elapsed this tick (constant angular velocity in sim-time — at very high warp dt is large enough that the slew completes in one tick, which is the accepted "effectively instant at high warp" behaviour).

Guards:

  • commanded ≈ 0 (undefined direction, e.g. pre-launch surface mode): no-op, hold the current nose.
  • CurrentAttitudeDir ≈ 0 (uninitialized: fresh spawn, legacy save, never-ticked test craft): SNAP to commanded. This is the load-bearing init guard — it prevents slewing from a garbage vector and prevents a nose teleport on a pre-v0.10.0 reload.
  • already within slewAngleEps, or the cap covers the whole angle: snap exactly to commanded (clean convergence, no residual jitter).
  • antiparallel (180°, degenerate cross product): pick an arbitrary unit perpendicular so the rotation axis is defined.

v0.10.0+.

func (*Spacecraft) SurfaceLatLon added in v0.21.0

func (s *Spacecraft) SurfaceLatLon() (lat, lon float64)

SurfaceLatLon returns the craft's body-fixed surface coordinates in degrees (north-positive, east-positive): the soft-touchdown coords when set, else the launchpad-spawn coords ("when non-zero, read these instead"). Meaningful when the craft is Landed. Single source of truth for both the landed-integration pin (sim.integrateLanded) and the mission evaluator's surface position (sim.missionEvalContext). v0.21+.

func (*Spacecraft) SyncFields added in v0.9.1

func (s *Spacecraft) SyncFields()

SyncFields refreshes the historical flat fields on s from the current Stages slice. Mass + propellant fields sum across all stages (the player's HUD wants total propellant); engine fields (Thrust, Isp, RCSThrust, RCSIsp) read from Stages[0] (the bottom = currently-firing). No-op when Stages is empty — callers that build Spacecraft without Stages (legacy test fixtures with literal Spacecraft{}) keep their flat-field values intact.

Call SyncFields after any mutation that changes a stage entry (burn, RCS pulse, decouple, dock). Reads are direct field access; the sync runs only on writes so the per-tick read path stays free of indirection.

func (*Spacecraft) ThrustAccelFn added in v0.2.1

func (s *Spacecraft) ThrustAccelFn(mode BurnMode, mu float64) func(r, v orbital.Vec3, t float64) orbital.Vec3

ThrustAccelFn returns an RK4-compatible accel closure that adds engine thrust along the given burn-mode direction on top of two-body gravity, using the spacecraft's live throttle. Routed through ThrustAccelFnAt so the manual-burn integrator path picks up live throttle adjustments. Direction is recomputed each sub-step from live (r, v), so prograde follows the rotating velocity frame — the expected UX for held-prograde burns.

Mass is held constant for the closure (the integrator treats the sub-step as ~constant-mass); the caller updates fuel via MassFlowRate after the StepRK4 call. Thrust is gated to zero if fuel is empty.

func (*Spacecraft) ThrustAccelFnAt added in v0.7.6

func (s *Spacecraft) ThrustAccelFnAt(mode BurnMode, mu, throttle float64) func(r, v orbital.Vec3, t float64) orbital.Vec3

ThrustAccelFnAt is ThrustAccelFn but uses an explicit throttle — used by the active-burn path in sim/world.go to honour the per-node throttle captured on the ActiveBurn struct at fire-time. Decoupling from `Spacecraft.Throttle` means adjusting the live throttle knob mid-coast doesn't slow a planted burn. v0.7.6+.

Wrapper: passes zero target state. Target-relative modes degrade to no-op without a snapshot. v0.9.3+: prefer ThrustAccelFnAtWithTarget when the caller has resolved target state.

func (*Spacecraft) ThrustAccelFnAtWithTarget added in v0.9.3

func (s *Spacecraft) ThrustAccelFnAtWithTarget(mode BurnMode, mu, throttle float64, rT, vT orbital.Vec3) func(r, v orbital.Vec3, t float64) orbital.Vec3

ThrustAccelFnAtWithTarget is ThrustAccelFnAt with a target-craft state snapshot captured at closure construction. The four target- relative modes (BurnTargetPrograde / Retrograde / BurnTarget / AntiTarget) resolve their direction against (rT, vT). The target moves during a sub-step but slowly relative to the per-step granularity, so freezing the snapshot per call is safe — the world layer reconstructs the closure each stepThrust pass with a fresh snapshot.

Pass zero rT, vT when no craft target is set (target-relative modes degrade to no-op, non-target modes are unaffected). Both states must be in the same frame as the closure's incoming (r, v) — the world layer (targetStateRelativeToActivePrimary) handles cross- primary conversion before construction.

v0.9.3+.

func (*Spacecraft) ThrustAccelFnFixedDir added in v0.10.0

func (s *Spacecraft) ThrustAccelFnFixedDir(dir orbital.Vec3, mu, throttle float64) func(r, v orbital.Vec3, t float64) orbital.Vec3

ThrustAccelFnFixedDir is the v0.10.0 slew-mode counterpart of ThrustAccelFnAtWithTarget: instead of recomputing the thrust direction from a BurnMode per RK4 sub-step, it thrusts along a FIXED world-frame unit vector for the whole tick — the craft's physical nose (CurrentAttitudeDir), already slewed toward the commanded direction once at the tick top and already carrying mode + pitch-trim resolution (it was integrated from BurnDirectionWithTarget). Freezing it for the tick is correct under the constant-rate, no-intra-tick-dynamics decision; the cosine loss from burning while the nose still lags the commanded direction emerges naturally. A zero/degenerate dir => gravity only (no thrust, matches the BurnDirection "undefined direction" no-op convention).

v0.10.0+.

func (*Spacecraft) TotalMass

func (s *Spacecraft) TotalMass() float64

TotalMass returns dry + fuel + monoprop.

type Stage added in v0.9.1

type Stage struct {
	// DryMass is the empty-tank mass of this stage in kg —
	// engine + structure, no propellant.
	DryMass float64

	// FuelMass is the current main-engine propellant mass in kg.
	// Decrements during finite burns; the burn engine reads from
	// Stages[0].FuelMass (the bottom stage's tank).
	FuelMass float64

	// FuelCapacity is the max main-engine propellant load in kg.
	// Determines undocking proportional shares + spawn-form
	// readouts.
	FuelCapacity float64

	// Thrust is the main-engine thrust in N. Zero disables the
	// main engine for this stage (RCS-only stages or empty
	// boosters).
	Thrust float64

	// Isp is the main-engine specific impulse in seconds. Used
	// by the rocket equation in finite-burn integration.
	Isp float64

	// MonopropMass is the current RCS-pool propellant mass in kg.
	// Per-stage: each stage carries its own RCS budget. The
	// active craft's RCS reads from Stages[0].MonopropMass when
	// the bottom stage has RCS, falling back to upper stages
	// only after the bottom is empty (or via a future explicit
	// RCS-source-stage selector — for v0.9.1, bottom-only).
	MonopropMass float64

	// MonopropCap is the max RCS-pool capacity in kg.
	MonopropCap float64

	// RCSThrust is the per-stage RCS thrust in N.
	RCSThrust float64

	// RCSIsp is the per-stage RCS specific impulse in seconds.
	RCSIsp float64

	// BallisticCoefficient (v0.9.2.1+) is C_D · A / m in m²/kg —
	// the multiplicative factor in the drag equation
	// a = -0.5 · ρ · |v_rel|² · BC · v̂_rel. Per-stage so a Saturn
	// V's S-IC booster (huge cross-section, ~3 Mkg mass, BC ≈ 8e-6)
	// drags differently than its S-IVB upper stage (small craft,
	// ~120 kkg mass, BC ≈ 6e-5). Zero falls back to
	// DefaultBallisticCoefficient at the Spacecraft level.
	BallisticCoefficient float64

	// LoadoutID names the catalog entry that originally produced
	// this stage. Used by save round-trip + spawn-as-passive on
	// jettison so the dropped stage gets the right glyph + colour.
	// Empty when the stage came from a manual construction; the
	// jettison path falls back to a generic "stage" identity.
	LoadoutID string

	// Name is the per-stage display label (e.g. "S-IC", "S-II",
	// "S-IVB"). Used by the STAGES HUD block + the spawn-as-
	// passive jettison path. Empty falls back to LoadoutID.
	Name string

	// Glyph + Color override the canvas marker for this stage
	// when it's jettisoned and spawned as a passive craft. Empty
	// resolves via LoadoutID lookup (default: VesselGlyph /
	// "#FFD93D" like the S-IVB-1 main loadout). Every catalog craft
	// uses VesselGlyph — vessels are told apart by Colour, not shape
	// (ADR 0020 reserves the geometric glyphs for orbital markers).
	Glyph string
	Color string

	// LaunchSpriteRowsPx is the per-stage height (in braille
	// sub-pixels) of this stage's silhouette in the ViewLaunch
	// chase-cam scene. Stack composes bottom-to-top from Stages[0]
	// along CurrentAttitudeDir; each stage paints a
	// (spriteWidthPx × LaunchSpriteRowsPx) filled rectangle of
	// braille dots via PlotColored. Zero means "no sprite, fall
	// back to the vessel-level Glyph render." Pivoted from ASCII
	// glyphs to braille pixels in v0.11.3 after playtest showed
	// box-drawing characters smear at gravity-turn angles
	// (see designdocs/terminal-space-program/v0.11-plan.md "Resolved at slice-open").
	LaunchSpriteRowsPx int `json:",omitempty"`

	// LaunchSpriteWidthPx (v0.11.5) is the per-stage width (in
	// braille sub-pixels) of this stage's ViewLaunch silhouette.
	// Zero falls back to the renderer's default width (2 px — the
	// pre-v0.11.5 universal constant), so un-catalogued and pre-
	// v0.11.5 stages keep their original 2-wide rectangle. Practical
	// range [1, 5] sub-pixels; catalog values are stylised, not
	// physics-derived. Each stage paints a width × LaunchSpriteRowsPx
	// rectangle centred on the stack axis — no auto-clamping based on
	// neighbour widths; the catalog author tunes which stage-to-stage
	// boundaries hard-step vs taper (see inter-stage taper rule in
	// launch_sprite.go).
	LaunchSpriteWidthPx int `json:",omitempty"`

	// LaunchSpriteColor (v0.11.5-followup) overrides Color for the
	// launch-sprite body / engine bell / inter-stage taper / landing
	// legs. Decouples slate HUD identity (Color, glyph palette) from
	// silhouette identity: catalog authors can keep distinct slate
	// hues per stage while painting the full stack in a unified
	// rocket-body palette so a 5-stage Apollo Stack doesn't read as
	// a rainbow gradient on screen. Empty falls back to Color so
	// un-catalogued and pre-v0.11.5 stages keep their existing
	// silhouette colour.
	LaunchSpriteColor string `json:",omitempty"`

	// FuelType (v0.11.5) names the main-engine propellant chemistry
	// so the renderer can tint the exhaust flame to a per-fuel
	// palette. Values: "kerolox" (RP-1 + LOX, orange — F-1, Merlin),
	// "hydrolox" (LH2 + LOX, pale cyan — J-2, RS-25, RL-10),
	// "hypergolic" (Aerozine 50 + N2O4, yellow-amber — LM descent,
	// SPS), "solid" (APCP, orange-red — SLS SRB). Empty/unset
	// preserves pre-v0.11.5 amber flame (render.ColorWarning).
	// Stages with Thrust == 0 (pure-monoprop RCS tugs) leave this
	// unset — no main engine ⇒ no flame ⇒ no fuel-type read.
	FuelType string `json:",omitempty"`

	// LaunchSpriteHasLegs (v0.11.5) opts a stage into the diagonal
	// landing-leg silhouette. When true AND the stage is Stages[0],
	// the renderer paints two splayed diagonal lines of sub-pixels
	// from the stage's bottom corners outward and downward, mirrored
	// about the stack axis, in Stages[0].Color. Painted in the
	// (stack-dir, width-dir) basis so legs lean with the rocket
	// through gravity turns (the v0.11.3 direction-agnostic
	// invariant). Suppressed when the stage is NOT at Stages[0] —
	// upper-stage landing legs don't read as the iconic Lander
	// silhouette. Today's only true entry: the Apollo Lander.
	LaunchSpriteHasLegs bool `json:",omitempty"`

	// CanSoftLand (v0.11.4-followup): per-stage flag for the
	// surface-arrival predicate. v0.11.4's first cut put this on
	// the Loadout level, which broke the Apollo-Stack flow — the
	// Lander stage couldn't carry its own soft-land capability
	// across a decouple into a freshly-spawned slate craft. Moving
	// the flag to the Stage level matches how other "per-stage
	// engineering" attributes work (BallisticCoefficient,
	// LaunchSpriteRowsPx) and SyncFields can re-derive
	// Spacecraft.CanSoftLand from Stages[0] on every staging /
	// dock / save-load. Set true on stages designed to land —
	// today: `lander` and `f9-s1` in StageCatalog.
	CanSoftLand bool `json:",omitempty"`

	// HasParachute (v0.12 Slice 3, ADR 0008): per-stage parachute
	// capability, the exact mirror of CanSoftLand. Set in StageCatalog
	// (today: the `csm` stage and the standalone re-entry capsule);
	// SyncFields re-derives Spacecraft.HasParachute from Stages[0] on
	// every staging / dock / load so the capability rides the hardware
	// across a decouple. `omitempty`.
	HasParachute bool `json:",omitempty"`

	// CommandSource (v0.23 / ADR 0027) declares whether this stage is a
	// control point: CommandCrewed / CommandProbe / CommandNone (empty).
	// Authored on the Part (ADR 0026 catalog) and carried here so it
	// rides the hardware across staging/dock/save; SyncFields derives the
	// vessel-level Crewed / Controllable mirrors from the whole stack.
	CommandSource string `json:",omitempty"`

	// AntennaKind / AntennaRangeM (v0.23 / ADR 0027) declare this stage's
	// comms hardware: kind (AntennaNone / AntennaDirect / AntennaRelay) plus a
	// rated range in metres — the distance at which it reaches an identical
	// antenna (the CommNet combinability model; see sim.commLinkRangeM).
	// Authored on the Part; SyncFields derives the vessel-level antenna (the
	// longest-ranged one in the stack) for the connectivity graph.
	AntennaKind   string  `json:",omitempty"`
	AntennaRangeM float64 `json:",omitempty"`
}

Stage describes one decouplable propulsion module on a spacecraft. v0.9.1+. The Stages slice on Spacecraft is the source of truth for dry mass / fuel / engine numbers; the historical flat fields (DryMass, Fuel, Thrust, Isp, Monoprop, MonopropCapacity, RCSThrust, RCSIsp) are derived shadow-mirror values refreshed by SyncFields after any stage mutation. Read sites keep using the flat fields; write sites must mutate the relevant stage entry and call SyncFields to keep the mirror coherent.

Convention: Stages[0] is the BOTTOM stage — the currently-firing engine and the next to be jettisoned by World.StageActive. Stages[len-1] is the TOP stage (the player's "core" — the only one left after every lower stage has been decoupled).

During ascent on a Saturn V (Stages = [S-IC, S-II, S-IVB]), the S-IC bottom stage provides the firing thrust + Isp; total mass sums dry + fuel across all three stages. Pressing `space` pops Stages[0] (S-IC) and spawns it as a passive Spacecraft; the active craft is now [S-II, S-IVB] with S-II as the new bottom stage / firing engine.

Single-stage craft carry exactly one Stage; on staging that stage is the bottom + top simultaneously. World.StageActive declines to drop the only stage of a single-stage craft (no-op + status flash) so the player doesn't accidentally jettison their core.

func BuildModule added in v0.13.0

func BuildModule(id string) ([]Stage, bool)

BuildModule returns the stage(s) a single configurator pick contributes to a custom stack, bottom-first. Most catalog ids map to exactly one stage; the "lander" id expands to the 2-stage Apollo-LM — Descent (bottom: legs + soft-land + powered-descent engine) + Ascent (top: returns to orbit) — so the configurator adds the lander as one vessel, the way the standalone Lander loadout ships it, rather than as two parts the player must stack in the right order. Unknown ids return ok=false (mirrors BuildStage).

func BuildStage added in v0.10.1

func BuildStage(id string) (Stage, bool)

BuildStage returns a fresh, full-tank Stage for the given catalog ID, with the catalog's default RCS pool (same DefaultRCSLoadout scaling stageWithBC uses). Unknown IDs return the zero Stage and ok=false so callers can reject rather than silently spawn junk.

func ComposeStage added in v0.24.0

func ComposeStage(componentIDs []string, comps map[string]Component) (Stage, string)

ComposeStage aggregates a list of component IDs into a runtime Stage, deriving the RCS pool from dry mass exactly as the loadout-resolve path does (DefaultRCSLoadout) so a VAB preview matches the eventually-spawned craft. Returns a non-empty warning string when the components don't form a valid stage (an unknown component, or mixed fuel chemistry — ADR 0029 §3); the returned Stage is then the zero value. Used by the VAB screen (internal/tui/screens/vab.go), which can't reach the unexported aggregation internals.

type StageModule added in v0.10.1

type StageModule struct {
	// ID is the stable catalog key (kebab-case). Referenced by the
	// configurator and BuildStage.
	ID string
	// Name / Glyph / Color are copied onto the built Stage so the
	// HUD + jettison rendering have a label and marker.
	Name  string
	Glyph string
	Color string
	// Tier is a one-word configurator hint: "booster", "sustainer",
	// "transfer", "payload", "tug". Purely descriptive.
	Tier string

	// VabSeed (v0.25 / ADR 0032 §6) is the part's optional crack-open seed
	// — the component-ID list the VAB expands this atomic block into when the
	// player presses enter on its stage header. Carried through from
	// Part.VabSeed; nil for parts with no authored decomposition. Never
	// touches the built Stage (BuildStage ignores it) — it is a VAB-editing
	// convenience only.
	VabSeed []string
	// contains filtered or unexported fields
}

StageModule is one catalog part: a stage preset plus the metadata the configurator UI needs to list and describe it.

type Target added in v0.9.3

type Target struct {
	Kind    TargetKind
	BodyIdx int    // when Kind==TargetBody
	CraftID uint64 // when Kind==TargetCraft or TargetGhost — the target's stable Spacecraft.ID (ADR 0012)

	// GhostOwner names the remote player (key fingerprint) when
	// Kind==TargetGhost (v0.27 S6, ADR 0034). Session-local by
	// nature: a save that carries a dangling ghost ref just resolves
	// to nothing on load.
	GhostOwner string `json:"ghost_owner,omitempty"`
}

Target identifies what a single craft is aiming at. The zero value (TargetNone) is the v0.9.0 default and round-trips through save as an absent JSON field. v0.9.3+ : every Spacecraft holds its own Target value so per-craft targeting persists across active- craft switches.

type TargetKind added in v0.9.3

type TargetKind int

TargetKind enumerates what a craft is aiming at. Type lives on `spacecraft` (not `sim`) so each Spacecraft can carry its own per-craft target as a struct field — moved from sim/target.go in v0.9.3 polish to support per-craft target binding (each vessel remembers its own target across active-craft switches).

`sim` re-exports the type via an alias so existing readers (`w.Target.Kind == sim.TargetCraft`) continue to compile unchanged.

const (
	// TargetNone — no target set. Planners that consume Target fall
	// back to their kind-less default (equatorial inclination match,
	// "pick a body cursor first" status flash).
	TargetNone TargetKind = iota
	// TargetBody references a body by index in System().Bodies.
	TargetBody
	// TargetCraft references a non-active craft by its stable
	// Spacecraft.ID (v0.14.x / ADR 0012; was a World.Crafts index).
	TargetCraft
	// TargetSite is reserved; not populated until landing-site
	// targeting ships post-v0.9.
	TargetSite
	// TargetGhost references another player's craft by owner
	// fingerprint + craft ID (v0.27 S6, ADR 0034). Resolves against
	// the world's transient ghost slate — a stale ref (owner gone,
	// craft staged away) resolves to nothing, same as TargetNone.
	TargetGhost
)

type TriggerEvent added in v0.8.1

type TriggerEvent int

TriggerEvent selects how a node's TriggerTime is determined. v0.6.0+.

Absolute (zero value) preserves the v0.1–v0.5 semantics: TriggerTime is set explicitly at plant time and never changes.

The event-relative modes leave TriggerTime zero at plant time; the resolver in sim's executeDueNodes computes TriggerTime once at the first Tick where the live orbit yields a future crossing (lazy freeze). After resolution the node behaves like an Absolute node.

v0.8.1+: lifted from internal/sim into internal/spacecraft so each Spacecraft can own its own []ManeuverNode without an import cycle.

const (
	TriggerAbsolute TriggerEvent = iota
	TriggerNextPeri
	TriggerNextApo
	TriggerNextAN
	TriggerNextDN
	// TriggerNextClosestApproach (v0.9.3+) resolves to the next
	// time-to-encounter between the active craft and a target craft
	// captured at plant time via ManeuverNode.TargetCraftIdx. Lazy-
	// frozen on the first tick after plant the same way AN / DN are.
	// Only valid for the four target-relative burn modes; pickable
	// in the m-form only when World.Target.Kind == TargetCraft at
	// plant time.
	TriggerNextClosestApproach
)

func (TriggerEvent) String added in v0.8.1

func (e TriggerEvent) String() string

String returns a human-readable label for the trigger event.

type VehicleStats added in v0.24.0

type VehicleStats struct {
	StageDV    []float64
	TotalDV    float64
	TotalMass  float64
	LiftoffTWR float64
}

VehicleStats summarizes a bottom-first stage stack for the VAB readout (ADR 0029 §5). It is a pure function of the resolved Stages, recomputed on every edit so the builder always shows live numbers.

  • StageDV is the per-stage ideal Δv (m/s), bottom→top: stage i fires hauling itself plus everything above it (the drop-stage chain), then is jettisoned. Mirrors the convention of the existing loadout Δv tests (RCS monoprop excluded — this is main-engine ideal Δv).
  • TotalDV is the sum of StageDV.
  • TotalMass is the wet mass on the pad (dry + main fuel + RCS monoprop), across every stage, kg.
  • LiftoffTWR is the bottom stage's thrust over the full stack weight at g0 (the reference launch gravity — the VAB designs a template, not a spawn on a specific body; the Apollo / Kern liftoff-TWR tests use the same g0 reference).

func StackStats added in v0.24.0

func StackStats(stages []Stage) VehicleStats

StackStats computes the VAB readout for a bottom-first stage stack.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL