Create / Modding reference / Actions
Actions
Everything a handler can DO. Actions run in authored order once every filter
passes; each is a newtype variant - Name((field: value, ...)), double
parens even for one field. Failures warn and continue (a missing target id
never panics a scenario). All 52 at a glance:
| action | group | what it does |
|---|---|---|
SpawnScenarioObject |
world | spawn one object: anchor, asteroid, planet, ship, beacon, or light |
ScatterObjects |
world | spawn count copies of a template at deterministic random positions |
DespawnScenarioObject |
world | remove a scoped object and its whole child hierarchy |
CreateScenarioArea |
world | spawn an invisible spherical sensor zone for OnEnter / OnExit |
Objective |
mission | post (or update in place) a HUD objective |
ObjectiveComplete |
mission | complete and remove the HUD objective with an id |
ObjectiveMarkerAttach |
mission | pin the gold HUD marker chip on a scoped object |
ObjectiveMarkerDetach |
mission | remove that marker |
NarrativeCue |
mission | speak one line into the comms stack |
HudReadout |
mission | bind a live HUD readout to a scenario variable |
HintEmphasisSet |
mission | pulse one keybind-dock chip gold |
HintEmphasisClear |
mission | drop the gold emphasis on one chip |
PlaySound |
sound | play one authored cue, heard in the cockpit |
Sequence |
pacing | run an ordered list of beats, each behind its own delay or gate |
Cinematic |
pacing | run the same chain as a SCENE the player is allowed to leave |
CancelCinematic |
pacing | end a running scene from the scenario rather than the player |
CinematicTitle |
pacing | post the shot's title card: where, when, and one line about it |
Outcome |
flow | show the VICTORY / DEFEAT banner and freeze the sim behind it |
NextScenario |
flow | queue a switch to another scenario by id |
BackdropDone |
flow | report a menu backdrop's act finished; the menu picks what plays next |
SetShipCapabilityStop |
ship state | grant or withhold STOP on a ship |
SetShipCapabilityGoto |
ship state | grant or withhold GOTO on a ship |
SetShipCapabilityOrbit |
ship state | grant or withhold ORBIT on a ship |
SetShipCapabilityLock |
ship state | grant or withhold the radar LOCK on a ship |
SetShipCapabilityRcs |
ship state | grant or withhold RCS on a ship |
SetShipCapabilityPointDefense |
ship state | let a ship's turrets answer incoming ordnance, or stand them down |
SetAllegiance |
ship state | overwrite a ship's side at runtime |
MoveShipTo |
ship state | fly an ordered ship to a mark and report when it arrives |
ForceAlign |
ship state | turn an ordered ship's nose onto a point and hold it there |
StopShip |
ship state | brake an ordered ship with the real STOP burn |
PatrolShip |
ship state | fly one loop of an authored waypoint route, back to where it started |
OrbitShip |
ship state | put a ship in a stable ring around a gravity well and hold it |
ClearShipOrder |
ship state | cancel whatever helm order a ship is under |
ZeroShipMotion |
ship state | take a ship's velocity away behind a cut, the player's included |
ForceRailgunFire |
ship state | fire one named railgun section |
ForceTorpedoFire |
ship state | launch one named torpedo bay at a named target |
SetInfiniteAmmo |
ship state | suspend or restore the finite magazine on every weapon of a ship |
RefillAmmo |
ship state | top a ship's magazines back up, or just one section's |
SetAILeash |
ship state | tether an AI ship's combat to a centre and radius, or release it |
SetAIEngageRange |
ship state | change how far an AI ship looks for hostiles, or restore the default |
SetAIPointDefenseRange |
ship state | change how close an inbound torpedo gets before the guns answer |
VariableSet |
variables | evaluate an expression and store the result in a variable |
TimerStart |
variables | start (or restart) a keyed scenario timer |
TimerCancel |
variables | cancel a running timer |
DebugMessage |
variables | log a line in debug builds |
SetCamera |
camera | pin the scenario camera at a pose |
SetCameraAnchor |
camera | ride the camera on an object, framing what you name |
ReleaseCamera |
camera | hand the camera back to the player's chase rig |
SuspendPlayerControl |
camera | block human gameplay input and clear held intent |
ResumePlayerControl |
camera | restore human gameplay input |
Screenshot |
camera | capture the primary window to a PNG |
SetSkybox |
camera | swap the scenario's skybox mid-scenario |
Scoped targets. Every by-id action resolves its id ONLY among
scenario-scoped entities (things this scenario spawned); the ship actions
additionally require the id to be a ship ROOT. This is deliberate: ship
SECTIONS carry per-ship ids like "controller" too, and an unscoped lookup
would hit every ship's section. Referencing an id before it is spawned warns
and does nothing - spawn first.
Spawning & the world
SpawnScenarioObject
Spawn one object. base is the shared identity block; kind picks the
object and carries its config - the seven kinds are the
Scenario objects reference.
SpawnScenarioObject((
base: (id: "rock_1", name: "Rock", position: (100.0, 0.0, -400.0), rotation: (0.0, 0.0, 0.0, 1.0)),
kind: Asteroid((radius: 50.0, texture: "dep://base/textures/asteroid.png", kind: "rock", initial_velocity: (0.0, 0.0, 0.0))),
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
base |
object base | required | identity + pose (below) |
kind |
object kind | required | Anchor((..)) / Asteroid((..)) / Planet((..)) / Spaceship((..)) / Beacon((..)) / Light(..) |
The base block:
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | the object's scenario id - the address every event, filter and by-id action uses |
name |
string | required | display name |
position |
3-tuple | required | world position, meters |
rotation |
4-tuple | required | world rotation quaternion (x, y, z, w) |
The same spawn id twice in ONE handler is a lint Error; the same id across two handlers (an either-or fork) is a Warn.
ScatterObjects
Spawn count copies of a template at deterministic random positions - the
declarative asteroid-field primitive. Copy i gets id "{id_prefix}{i}".
ScatterObjects((
id_prefix: "asteroid_",
count: 20,
seed: 433757350076153856,
region: Box(min: (-1000.0, -200.0, -1000.0), max: (1000.0, 200.0, 1000.0)),
template: (
base: (id: "asteroid_", name: "Asteroid", position: (0.0, 0.0, 0.0), rotation: (0.0, 0.0, 0.0, 1.0)),
kind: Asteroid((radius: 10.0, texture: "dep://base/textures/asteroid.png", kind: "rock", initial_velocity: (0.0, 0.0, 0.0))),
),
asteroid_radius: Some((10.0, 30.0)),
asteroid_kinds: [("rock", 12), ("carbon", 4), ("ice", 3), ("metal", 1)],
min_separation: Some(320.0),
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id_prefix |
string | required | id prefix for the copies (a filter id starting with this prefix lints clean) |
count |
integer | required | copies; runtime cap 4096, absurd counts are a lint Error |
seed |
integer | required | RNG seed - the same seed gives the same layout every load. Asteroid templates without an authored seed also get deterministic per-rock silhouette seeds derived from it, so the field's shapes are stable too |
region |
region | required | sampling volume in meters (below) |
template |
object config | required | the object each copy clones (any kind; same base/kind shape as SpawnScenarioObject) |
asteroid_radius |
Option (lo, hi) |
None |
Asteroid templates only: randomize each rock's radius, in meters, in [lo, hi) |
asteroid_kinds |
list of (kind, weight) | required on an asteroid template | the field's KINDS and how common each one is. Weights are relative COUNTS, not percentages: [("rock", 12), ("metal", 1)] is one metal body in thirteen. Empty on any other template |
min_separation |
Option number |
None |
an EXTRA centre-to-centre floor in meters, on top of the automatic body-to-body clearance below. Measured against every body scattered so far this scenario, earlier scatters included; 64 placement tries per copy, unplaceable copies are DROPPED, never overlapped |
region variants (struct variants - single parens, named fields):
| variant | fields | meaning |
|---|---|---|
Box(min: (..), max: (..)) |
both required | uniform per axis in [min, max] |
Ring(center: (..), inner: .., outer: .., y_min: .., y_max: ..) |
all required | horizontal annulus: uniform angle, radius in [inner, outer], y in [y_min, y_max] |
asteroid_kinds is drawn from the scatter's OWN seed, on a stream of its own:
adding a kind to a field that already ships moves no rock and changes no
radius. An asteroid template with no mix, or with every weight at zero, is a
lint error and spawns nothing - a belt has to say what it is made of.
Bodies are kept clear of each other WITHOUT you sizing anything. Every
placement records its own reach - a rock's noise mesh reach (up to 6x the
nominal radius, drawn from the same seed the body will use), a planet's
surface radius, zero for anything with no body - and a candidate is rejected
inside existing reach + candidate reach. A field of mixed sizes is therefore
packed by the bodies it actually holds, not by one number sized for its
biggest pair. This checks earlier ScatterObjects placements only: a planet
spawned by SpawnScenarioObject is not in that set. Keep the entire field
outside each planet's sphere of influence plus the largest rock's physical
reach if its template starts at zero velocity. Otherwise a rock can spawn
inside the planet or start falling toward it; lint cannot infer a safe orbit
from one velocity shared by every randomly positioned copy.
min_separation is what you add ON TOP of that: a spacing you want for
gameplay rather than for geometry - a belt you can fly through, a picket line
that reads as a line. The rule is max(min_separation, reach + reach), so a
value smaller than two bodies side by side changes nothing.
A randomized asteroid_radius is drawn BEFORE placement, on a stream of its
own, so a rock is the same size however many tries its neighbourhood cost.
Scatter results are gameplay content and are never thinned by graphics
quality.
DespawnScenarioObject
Remove the scoped object whose id matches (recursively, whole child hierarchy). The classic pairing is a beacon area the player has reached.
DespawnScenarioObject((id: "beacon_1")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | scoped object to despawn; missing id warns |
A body despawned while inside a trigger area fires NO OnExit for itself.
CreateScenarioArea
Spawn an invisible spherical SENSOR zone that drives
OnEnter / OnExit under its
id.
CreateScenarioArea((id: "safe_zone", name: "Safe Zone",
position: (0.0, 0.0, -1000.0), rotation: (0.0, 0.0, 0.0, 1.0), radius: 100.0)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | the id OnEnter/OnExit report as the area |
name |
string | required | display name |
position |
3-tuple | required | sphere centre, meters |
rotation |
4-tuple | required | rotation (cosmetic for a sphere) |
radius |
number | required | sensor radius, meters |
Works mid-scenario, and works even when created AROUND a body already inside (the entry still fires).
A beacon with area_radius is its OWN area - no separate
CreateScenarioArea needed for it.
Mission & story
Objective
Post a HUD objective. Objectives state goals; comms lines
(NarrativeCue) carry voice.
Objective((id: "destroy_asteroids", message: "Objective: Destroy 5 asteroids!")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | opaque handle ObjectiveComplete uses |
message |
string | required | the objective text on the HUD |
Re-posting the same id with new text updates the entry in place (the "recovered N/3" tally trick). A duplicate post otherwise warns.
ObjectiveComplete
Complete (remove) the HUD objective with this id.
ObjectiveComplete((id: "destroy_asteroids")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | the objective to complete; a never-posted id warns (and lints) |
ObjectiveMarkerAttach
Pin the gold HUD marker chip (label + live distance) on a scoped object.
ObjectiveMarkerAttach((target_id: "beacon_1", label: "BEACON 1")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
target_id |
string | required | scoped object to mark; spawn it first |
label |
string | required | short chip text ("BEACON 1") |
Re-attaching updates the label in place. A despawned target detaches implicitly.
ObjectiveMarkerDetach
Remove that marker.
ObjectiveMarkerDetach((target_id: "beacon_1")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
target_id |
string | required | scoped object to strip; a missing id is quietly fine (detach-after-despawn is legitimate) |
NarrativeCue
Speak one line into the HUD comms stack (bottom-left, arrival order, ~8 s hold each, at most three visible). Pending lines wait without being dropped. One line per beat is still the style; the queue is the safety net.
NarrativeCue((speaker: "Foreman Okono", text: "Strip it clean, Kestrel.", dwell: Some(12.0))),
NarrativeCue((speaker: "Copilot", text: "You heard the man.")),
NarrativeCue((
speaker: "Unknown Signal",
text: "- state registry and -",
accent: LinearRgba((red: 1.0, green: 0.55, blue: 0.2, alpha: 1.0)),
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
speaker |
string | required | the distinct uppercase speaker header |
text |
string | required | the line |
dwell |
Option number |
None |
per-line hold override in seconds, clamped to [3, 30] (lint warns outside); Some(12.0) |
icon |
Option asset ref |
None |
speaker portrait (Some("self://icons/okono.png")); omitted = the cockpit fallback tile |
accent |
color | the comms blue | the one colour the card is drawn in; see below |
The accent. A line carries ONE presentation fact, and it is a colour, not an id. The panel derives everything it paints from it: the border, the speaker header, the fallback icon tile, and the reading copy lifted toward white. The background is the same dark for every card, and a card's opacity depends only on its normal display and fade.
Leave it out and the line is drawn in the comms blue - the work channel, traffic
addressed to this ship, which is what nearly every line in a scenario is.
Authoring the blue on all of them would bury the few that differ, so the default
is written nowhere and the generated base scenarios carry no accent: line at
all.
Name a colour when the line is NOT ordinary traffic: a distress band, a
corporate net, a voice in the room off the radio, something the cockpit merely
overheard. Nothing looks an id up, so nothing can fail to resolve one, and a mod
invents as many voices as it has colours. Colour is tagged the same way it is
everywhere else in content - LinearRgba((..)) or Srgba((..)).
Two lines the player must tell apart should differ by more than a shade. The useful distance is being CALLED versus OVERHEARING, and blue against amber reads at a glance where two blues do not.
Pacing is yours, not the panel's. A card holds its full time whatever is waiting behind it - the panel never takes reading time back to catch up - so the gap between two POSTS is what sets the reading rate. Post a line every 3-4 seconds and the reader has three cards up, the newest shoving the others upward as it lands; six to seven seconds leaves the card being read as the only one moving. Budget more where the player is flying and less where a cut has taken the helm, and keep a line to one thought: a card that needs longer than its hold is two lines, not one line held open.
Scenario-scoped: teardown clears the log.
Two story lines in one handler is a lint Warn (unreadable); a story line
beside an Outcome in one handler is a Warn (frozen behind the
overlay). Let the outcome's own message carry the closing line.
HudReadout
Bind a live HUD readout to a scenario variable - the DISPLAY half of the variable vocabulary (a run clock, a score, a countdown).
HudReadout((slot: "run_timer", variable: "scenario_elapsed", format: Time, label: Some("TIME"))),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
slot |
string | required | stable readout id; several slots run side by side; fire again to update or clear |
variable |
string | required | the variable shown (e.g. "scenario_elapsed"); undefined or non-numeric reads 0.0 |
format |
format | Number |
Number (one decimal), Integer (rounded), Time (mm:ss.s) |
label |
Option string |
None |
caption before the value (Some("TIME")) |
visible |
bool | true |
true shows/updates; false clears exactly this slot |
One fire is enough; the readout tracks the variable's current value every frame thereafter. It freezes under pause and behind the outcome overlay because the variable does - a time-trial's final time simply holds through the banner. Cleared at teardown.
HintEmphasisSet
Pulse one keybind-dock chip gold - how a tutorial points at a key before granting it.
HintEmphasisSet((verb: "RADAR")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
verb |
string | required | one of "STOP", "GOTO", "ORBIT", "CANCEL", "RADAR", "COMPONENT", "RCS", "DOCK", "HELM"; unknown verbs warn and do nothing |
The dock normally hides verbs the player cannot use yet, so emphasizing an
unavailable verb REVEALS its chip dimmed and pulses it. Emphasis never
grants the capability (the
SetShipCapability* actions do).
HintEmphasisClear
Drop the gold emphasis on one chip (teardown clears all emphasis anyway).
HintEmphasisClear((verb: "RADAR")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
verb |
string | required | the chip to un-pulse |
Sound
PlaySound
Play one authored one-shot. Sound that BELONGS to something in the world - an intake's take ding, a hull coming apart - is authored on that object and played where it happens. This is the other kind: the cue a scene needs and nothing in the world produces.
PlaySound((sound: "self://sounds/warn_lock.wav", route: Interface)),
PlaySound((sound: "self://sounds/warn_hull.wav", route: Hull, volume: Some(0.4))),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
sound |
asset ref | required | the clip, resolved like any other content sound ref |
route |
Interface | Hull |
required | where it is heard |
volume |
Option number |
None |
gain; omitted is full volume, Some(0.4) sits a cue UNDER a comms line |
Interface is UI chrome - a prompt, a stinger, a chapter tone - scaled by the
interface track. Hull is structure-borne through the player's own ship,
something they feel rather than hear, scaled by the world track. The route is
authored because those are different cues and neither is the safe guess.
Non-positional on purpose. An exterior cue is attenuated and panned by where it happened, and a beat has no position to give one; a cue authored here is heard in the cockpit, which is where the player is. Author the positional kind on the object that makes it.
Pacing
Sequence
Run an ordered list of BEATS. The ENGINE holds the cursor, so a paced chain costs one action and no scenario variable.
Sequence((
key: "opening",
steps: [
(
after: Some(2.0),
actions: [
NarrativeCue((
speaker: "Capt. Halloran",
text: "Kestrel, you are cleared to burn.",
)),
],
),
(
after: Some(8.4),
actions: [
Objective((id: "b1_burn", message: "Fly to BEACON 1.")),
],
),
],
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
key |
string | required | scenario-local chain key; the engine files the cursor under it |
steps |
list | required | the beats, in order; at least one |
A step:
| field | type | default | meaning |
|---|---|---|---|
after |
seconds | none | scenario time to wait, from when this step became current |
until |
event + filters | none | an event that must arrive, qualified by its filters |
deadline |
seconds | none | how long the step may wait before the chain is called stuck; REQUIRED with until |
actions |
list | [] |
what the beat does |
Both waits may sit on one step, and they run TOGETHER: a gate that opens early still owes the delay, and a delay that elapses first still owes the gate. A step with neither runs the moment it becomes current - which is how a hand-off beat rides the end of a chain.
(
after: Some(6.0),
until: Some((
name: OnUpdate,
filters: [Expression((Equal(
Term(Factor(Name("surveyed"))),
Term(Factor(Literal(Number(1.0)))),
)))],
)),
deadline: Some(600.0),
actions: [ /* the beat */ ],
),
The semantics are WAIT, never SKIP: a step whose gate stays shut blocks the
beats behind it. That is why a gated step must carry a deadline - when it
expires the chain STOPS and logs an error, so a soft-lock is loud instead of
silent. content lint refuses a gated step without one.
Delays ride the pause-frozen scenario clock, so a gap measures play time, not wall time.
One cursor per key. Starting a key whose chain is still running is refused and logged; a chain that has finished frees its key. Several handlers MAY start the same key when only one of them can ever fire - every win variant of a scenario starting one shared outro chain is the idiom.
A step's actions are a FRAME of their own, landing seconds after the handler that queued them, and everything else applies inside a step unchanged: an objective still must not share a beat with a comms line, and a beat may start a chain of its own.
What it replaces. A paced chain used to be sibling OnUpdate handlers
strung together by hand: a counter variable seeded in OnStart, a
VariableSet per beat to advance it, and a filter per beat reading both the
counter and scenario_elapsed against a stamped deadline. All of that is
about the machine. Reach for a chain whenever the only thing a beat is waiting
for is "later"; keep a handler where the beat must still ASK something when it
lands - which of two lines to speak, whether the fight is still live - because
a step runs when its wait ends and only a handler can re-check.
Cinematic
Run the same beat chain as a SCENE - one the player is allowed to walk out of.
Mechanically a Sequence with a different contract: a sequence is
scenario logic and always runs to its end; a scene takes the camera and the
controls away, and a player who has seen it once must be able to get them back.
Cinematic((
key: "strike_approach",
skippable: true,
steps: [
(
after: Some(0.0),
actions: [
SuspendPlayerControl(()),
SetCameraAnchor((anchor: "cutter", offset: (440.0, 25.0, -235.0), frame: World, look_at: Object("warship"))),
],
),
(
after: Some(14.0),
actions: [
NarrativeCue((speaker: "Unknown Signal", text: "- vessel this net -", accent: LinearRgba((red: 1.0, green: 0.55, blue: 0.2, alpha: 1.0)))),
],
),
],
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
key |
string | required | scenario-local scene key; the cursor is filed under it and both cinematic events report it |
skippable |
bool | required | whether the player may leave early |
steps |
list | required | the beats, in order - the same after / until / deadline / actions a Sequence step has |
The two ends are EVENTS, not fields. A scene does not carry an on-finish action list; it REPORTS, and the handlers that answer live beside it:
OnCinematicFinishedfires on every path out - the last beat, a skip, aCancelCinematic, a blown step deadline. What the scene owes the player back (camera released, control returned, the objective it was covering) is authored there, ONCE, so no path can forget it.OnCinematicSkippedfires first on a skip, and carries only the catch-up: what the beats that never played would have left behind - the actors spawned, moved or destroyed, the variables latched.
Filter both with Cinematic((key: "...")), or a
scenario with two scenes answers the wrong one. A filter naming a key no
Cinematic plays is a lint Error, and a lint error refuses the scenario at
load.
skippable is authored on every scene because a scene that cannot be left is a
real decision - three seconds long, or the first time only - and not the kind of
thing that should happen because a field was left out.
skippable: true puts a prompt on the screen. The HUD prints
<KEY> SKIP SCENE bottom-centre, above the keybind dock, for as long as the
scene will take the key; it reads the player's live binding for the
cinematic_skip action, so a rebind is reflected mid-scene and a pad player is
shown a pad glyph. It appears when the 0.75 s hold-off below expires, not when
the scene opens, and it goes the moment the scene ends by any path. There is no
field to suppress it: the prompt IS the promise the flag makes, and it is one
of the two HUD surfaces the player's own hide-HUD toggle cannot take away.
A skip cancels the CURSOR. It never runs the remaining steps at speed: a step chain is not a list of world edits, it holds camera moves, comms lines and shots the player has just said they do not want. Whatever the world still needs belongs in the skipped handler.
The skip binding is held off for the first 0.75 s of the scene. The player is very often still holding the button that got them here, and without the hold-off a scene ends on the frame it opens and reads as a bug.
Nested chains outlive the skip. A Sequence started inside a scene has its
own cursor, and cancelling the scene does not cancel it. A line that must not
survive a skip belongs in the scene's own steps, not in a chain the scene
started.
CancelCinematic
End a running scene now, from the scenario rather than from the player.
CancelCinematic((key: "strike_approach")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
key |
string | required | the scene to end; a key no Cinematic plays is a lint Warn |
The scene still reports OnCinematicFinished,
so whatever it took is given back. It does NOT report a skip: nobody asked to
leave. Cancelling a key that is not running is a quiet no-op.
CinematicTitle
The shot's title card: where this is, when it is, and one line about it. Fades in top-left, holds, fades out.
CinematicTitle((
corner: TopLeft,
location: "SECTOR M, THE RINGS OF SATURN",
date: "Y0",
note: "The EWI carrier Meridian: a refinery that moves to the ice.",
seconds: 9.0,
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
corner |
TopLeft | TopRight | BottomLeft | BottomRight |
required | which corner the card sits in |
location |
string | required | where this is, drawn largest |
date |
string | required | when this is - a year, a date, a time of shift |
note |
string | required | one line of context; empty draws no line |
seconds |
float | required | how long the card holds, fades included |
Nothing takes the card down. It expires on its own clock, so a scene that
is skipped, cancelled, deadlined or torn down leaves a card that simply
finishes. There is no handler to write and no ClearCinematicTitle to forget.
One card at a time. Posting a second replaces the first at its own age - two cards on screen are two answers to "where am I".
The corner is authored because only the shot knows which one is free.
BottomLeft is the comms stack's corner, so a scene with dialogue in it wants
a top corner; the top strip carries the status bar, which the card clears.
A right-hand card mirrors a left-hand one: the accent rule stays on the outer
edge and the lines stack against it.
The clock is the scenario's, not the frame's. A card holds through a pause
rather than bleeding away behind the menu. A seconds shorter than the two
fades together peaks lower instead of snapping on and off, which is the honest
reading of "show this for half a second".
Flow: outcomes & transitions
Outcome
Declare the scenario's win or lose: the gold VICTORY / red DEFEAT banner, an optional message, and buttons - and it freezes the simulation behind it exactly like the pause menu until it clears.
Outcome((outcome: Defeat, message: Some("The cutter is lost."))),
NextScenario((scenario_id: "the_wreck", linger: true)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
outcome |
kind | required | Victory or Defeat |
message |
Option string |
None |
the line under the banner; strict RON Some("...") |
auto_advance_secs |
Option number |
None |
timed banner: advance the queued LINGERING switch after this many REAL seconds (the player can still click sooner); cap 300 s; meaningless without a lingering switch queued |
PRESENTATION ONLY: compose the consequence beside it. Queue a lingering
NextScenario and the overlay offers Continue (Victory) or
Retry (Defeat); queue nothing and it offers only Main Menu.
The paired switch must LINGER: an instant switch tears the scenario down the same frame and SWALLOWS the banner (lint warns on that composition).
NextScenario
Queue a switch to another scenario by id - a hard cut, a delayed cut, or a modal hold behind the outcome overlay.
NextScenario((scenario_id: "the_wreck", linger: true)),
Show explanation
Three gears:
- Hard cut -
linger: false, no delay: switches on the next sync. - Delayed cut -
linger: false, delay: Some(4.0): the world keeps playing for the delay (a story line can land), then cuts. Ticks on pause-frozen time; Enter skips the wait. - Modal hold -
linger: true: waits for the scenario-advance input (Enter / DPadDown) or the outcome overlay's Continue/Retry.
| field | type | default | meaning |
|---|---|---|---|
scenario_id |
string | required | target scenario; a dangling id is a lint Error |
linger |
bool | required | true defers until released; false switches now (or after delay) |
delay |
Option number |
None |
delayed non-lingering cut, seconds; cap 300 s, lint warns above 60; meaningless with linger: true |
The last request wins wholesale - a fresh NextScenario replaces a queued
one and resets its clock.
A menu backdrop may not author NextScenario (lint Error): it reports with
BackdropDone instead.
BackdropDone
Report that a menu backdrop's act is finished. The menu holds the shot for one second, then cuts to the next backdrop in its rotation.
BackdropDone(()),
Show explanation
No fields. The backdrop never names its successor: the menu plays every backdrop once, in a shuffled order, before any plays again, and never the same one twice in a row when it has two or more.
- Every
role: Backdropscenario must author one (lint Error otherwise). A scene with no natural ending carries a timer, like the base backdrops' 150 s rotation limit. BackdropDoneanywhere else is a lint Error: nothing reads it.- The one-second beat runs on pause-frozen time. A second report inside the beat changes nothing.
Ship state
Ship capabilities
Grant or withhold one capability on a scoped SHIP - the tutorial-progression primitive (Basic Training starts with everything but the guns withheld and grants one per beat, GOTO after the cadet's first lock).
One action per capability: SetShipCapabilityStop, SetShipCapabilityGoto,
SetShipCapabilityOrbit, SetShipCapabilityLock, SetShipCapabilityRcs,
SetShipCapabilityPointDefense. All six take the same two fields. The ship's
seventh capability, dock_enabled, is authored at spawn and has no action.
SetShipCapabilityGoto((id: "player_spaceship", enabled: true)),
// The battery answers a salvo by itself - until this takes it away.
SetShipCapabilityPointDefense((id: "player_spaceship", enabled: false)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | scoped ship root |
enabled |
bool | required | true grants, false withholds |
The capability lands on the ship ROOT, not on its flight computer: a hull that loses every controller section loses its steering, never its permissions. Never withhold STOP by convention - an engaged autopilot should always be cancelable.
The spawn-time twin is the
capabilities block on the spaceship object; these
actions are its runtime mirror.
SetAllegiance
Overwrite a scoped ship's side at runtime - the neutral-until-provoked
primitive: spawn a bystander Neutral, flip it Enemy on a trigger.
SetAllegiance((id: "magpie", allegiance: Enemy)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | scoped ship root; a dangling id is a lint Error |
allegiance |
side | required | Player / Enemy / Neutral (bare enum) |
Helm orders
The six helm actions below take the helm of a ship the scenario owns: one authored
with controller: None OR one authored with controller: AI (see
Spaceship). They refuse the PLAYER's ship - a lint
Error, and a runtime error if one somehow reaches the engine - because a
player's flight input drops any autopilot on the next frame, so the order
would fight the stick and lose.
An order on an AI ship OUTRANKS the bot. While the order runs, the AI stops writing to the helm; it keeps looking around and keeps shooting. This is the seam that lets a mission tell an ordinary patrol craft to go somewhere specific and then hand it back, instead of authoring a second inert hull.
The ship still needs to be a real ship: the helm orders fly it with its own flight computer and thrusters, so a hull with no live controller section cannot turn, and a battery with no drives can still shoot but never move.
Move, Align, Stop, Patrol and Orbit are ONE mutually exclusive
family. A ship holds at most one helm order; installing a second cancels the
first. Each is keyed, and the
order events with a
ShipOrder filter are how the next beat waits for it
- which is what lets a
Sequenceuntilgate hold a set piece together.
Some orders end and let go; others end and KEEP HOLDING. Move, Stop and
Patrol release the helm when they report, so an AI ship goes straight back
to its own routine. Align and Orbit report the condition and then hold it
until something else takes the helm - the hold is the point of both.
MoveShipTo
Fly an ordered ship to a mark and report when it gets there. The ship's own GOTO maneuver, so it accelerates, coasts and arrives on its authored thrust - this is the cinematic approach, not a teleport.
MoveShipTo((
order: "close_the_gap",
ship: "warship",
position: (0.0, 0.0, -1200.0),
arrival_standoff: Some(60.0),
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
order |
string | required | the key this order's completion is reported under; an empty key is a lint Error |
ship |
string | required | scoped None- or AI-controller ship root; a dangling or player-driven id is a lint Error |
position |
meters (x, y, z) |
required | the mark to fly to, in world coordinates |
arrival_standoff |
Option meters |
None |
the margin to leave between the ship's own hull and the mark; Some(0.0) parks the hull's face on it |
Completes when the ship's autopilot lets go - it is inside the standoff and
settled - and fires
OnShipOrderComplete with kind: Move.
arrival_standoff exists because the default 500 m is far too coarse to
stage a shot with. It is a MARGIN, not a centre distance: the ship comes to
rest with that much clear water between its own hull and the mark, so a
warship and a shuttle sent to the same mark both stop where you staged them.
It is installed for the life of the order and taken back off when the order
retires - on completion as well as on a cancel or an interruption - so a
cinematic's tight staging does not silently retune every later GOTO the hull
flies. None uses the ship's own standoff.
A margin is a PARKING rule, not a route guarantee. The autopilot plans no route and dodges nothing on the way in, and gravity can bend the leg; stage the approach yourself if something is in it.
ForceAlign
Turn an ordered ship's whole hull onto a point and HOLD it there. Rotation only - no autopilot, so no drive ever burns for translation and the ship keeps whatever velocity it had.
ForceAlign((
order: "bore_on_target",
ship: "warship",
look_at: (0.0, 0.0, 0.0),
tolerance_degrees: 1.5,
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
order |
string | required | the key this order's completion is reported under |
ship |
string | required | scoped None- or AI-controller ship root |
look_at |
meters (x, y, z) |
required | the world position to put under the bore |
tolerance_degrees |
number | required | how close the aim must come before the order completes; must be finite and not negative, or the order is refused |
The hold is the point. A railgun does not traverse, so the shot leaves down whatever line the hull is holding: align, wait for the completion, then fire, and the slug goes where you aimed. The facing is held until another helm order replaces it, which is what lets several guns run their charges on one bearing.
The tolerance is also what "settled" is measured against - a tight tolerance asks for a genuinely steady hull, a coarse one accepts a drift - so an alignment order on a tumbling wreck may take a while, and one with an impossible tolerance is refused outright rather than left never completing.
StopShip
Brake an ordered ship with the real STOP maneuver - the same flip-retrograde-and-burn the player's X key runs, so it costs fuel and time, visibly.
StopShip((order: "hold_here", ship: "warship")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
order |
string | required | the key this order's completion is reported under |
ship |
string | required | scoped None- or AI-controller ship root |
Completes when the maneuver ends, with kind: Stop. "Ended" is a deadband and
not a dead stop: a residual the drive is already pointing at is braked out to
2 m/s, and one it is not - a sideways drift, or a head-on crumb - is released at
up to 7.5 m/s rather than chased with attitude flips. A hull granted RCS settles
the remainder on its jets. So a ship already inside that band completes almost
immediately, and a beat that needs the hull genuinely still should gate on a
Speed query rather than on
this order alone.
PatrolShip
Fly ONE loop of an authored waypoint route: the ship visits each mark in order, returns to the first one, and reports. This is the sweep-and-come-back beat - not a standing assignment.
PatrolShip((
order: "sweep_the_belt",
ship: "picket",
waypoints: [
(0.0, 0.0, -800.0),
(900.0, 0.0, -800.0),
(900.0, 0.0, 0.0),
],
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
order |
string | required | the key this order's completion is reported under |
ship |
string | required | scoped None- or AI-controller ship root |
waypoints |
list of meters (x, y, z) |
required | the marks to visit, in order; an EMPTY list is a lint Error |
ONE loop, then kind: Patrol and the helm goes back. A route of one point is
one leg out and one leg home; a route with the same point twice in a row still
flies both legs, and the second lands immediately. Repeat the action to run
another loop, or hand the ship a
ClearShipOrder mid-route to abandon it.
For a STANDING patrol that never ends, author the route on the ship instead -
an AI ship's own patrol field (see Spaceship) is
the routine it returns to when nothing is happening.
OrbitShip
Put a ship in a stable ring around a gravity well and hold it there. The ship's own ORBIT maneuver, so it aligns, burns into the band, and stays.
OrbitShip((order: "take_the_ring", ship: "surveyor", well: Authored("planetoid"))),
OrbitShip((order: "park", ship: "surveyor", well: NearestToShip)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
order |
string | required | the key this order's completion is reported under |
ship |
string | required | scoped None- or AI-controller ship root |
well |
Authored(id) or NearestToShip |
required | Authored names one gravity-well object this scenario spawned; a dangling id is a lint Error. NearestToShip takes, among loaded wells with a stable ring, the one whose gravity owns the ship when the order engages, else the one whose surface is nearest, streamed wells included. The ring can still cross a neighbouring world. An order resumed after an interruption picks again and can take another well |
Completes with kind: Orbit the moment the ring is ESTABLISHED - the ship is
in the band and holding - and then keeps holding it, the same way
ForceAlign keeps its bearing. A completion here means "it is
in orbit now", not "it has stopped orbiting".
No loaded well that matches, two wells that match equally, or a well with no
stable band for this hull fails the order rather than leaving the beat
waiting: see OnShipOrderFailed. Two wells
at the same surface distance fall to the lower id.
If the well goes after the ring is established - a streamed sector unloads, the body is destroyed - the order retires without another event and the helm goes back to the ship. It does not pick the well up again if it reloads.
ClearShipOrder
Release a ship's helm. The counterpart to the five orders above: whatever the ship was told, it is no longer being told it.
ClearShipOrder((ship: "warship")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
ship |
string | required | scoped ship root to release |
A None-controller hull KEEPS its velocity and coasts - this is space, and
letting a ship drift out of frame is usually the point. An AI ship goes back
to its own routine on the next frame.
Emits OnShipOrderCanceled, NOT a
completion: an order that was called off did not finish, so a beat gated on
the completion correctly never runs. It also puts back the ship's own arrival
standoff if a MoveShipTo had displaced it. Clearing a ship
that is under no order does nothing and says nothing.
ZeroShipMotion
Take a ship's motion away outright: linear and angular velocity to zero, this frame, with no maneuver flown and no fuel spent.
NOT a helm order. It installs nothing, reports nothing, cancels nothing, and it is the one stop a PLAYER hull accepts.
ZeroShipMotion((id: "kaveri")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | scoped ship root, any controller |
This is the CINEMATIC counterpart to StopShip, and the only one
of the two a player's hull accepts. A helm order cannot share a helm with live
input, so StopShip refuses a player ship out loud; but a scene that cuts the
camera away for a minute of dialogue has to leave the player's ship somewhere
sane, and asking a suspended pilot to coast on through a rock field is not it.
Use it behind a cut, where the ship is off screen and the arithmetic is nobody's
business - season one's distress call does exactly that, and is worth reading.
It only ever takes speed off. A ship whose controller still wants to burn is
moving again on the next tick, so pair it with
SuspendPlayerControl for a player hull or with
ClearShipOrder for an ordered one. It reports no
completion: there is no maneuver to finish.
Forced fire
The two weapon actions below are independent of the helm and of each other: a ship can be aligned, firing its spinal gun and launching a bay in the same frame.
Unlike the helm orders, these still refuse an AI-driven ship as well as the player's. A bot picks its own targets and times its own shots, and a scripted trigger pull cutting across that would fight the same weapon seams every frame.
ForceRailgunFire
Fire one named railgun section of a scripted ship.
ForceRailgunFire((ship: "warship", section: "spinal")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
ship |
string | required | scoped None-controller ship root that fires |
section |
string | required | the authored section id of the railgun; a section the hull does not carry, or one that is not a railgun, is a lint Error |
No target and no steering: a railgun does not traverse, so the shot leaves
down whatever line the hull holds when the charge completes. Putting that line
on something is ForceAlign's job. Everything else is the
gun's own behavior - the authored charge time, the magazine, the reload, the
recoil through the hull, the slug, the sound and the flash.
The order is ONE SHOT: it holds the trigger until the gun actually fires (through a reload, if the magazine was empty) and then retires itself. Fire a second shell with a second action.
ForceTorpedoFire
Launch one named torpedo bay of a scripted ship at one named target.
ForceTorpedoFire((ship: "battery_west", section: "bay", target: "patrol_ship")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
ship |
string | required | scoped None-controller ship root that launches |
section |
string | required | the authored section id of the bay; a missing id or a non-bay is a lint Error |
target |
string | required | scoped ship root the ordnance homes on; a dangling id is a lint Error |
ONE bay, addressed by its section id. The bay's own cooldown and ammo still time the launch, and the ordnance is committed to the target the same way an AI or player launch is - which is also what makes the torpedo visible to hostile point defense. The AI's launch gates (range envelope, hull-forward cone, line of sight, the AI cadence) do NOT apply: the script is the decision.
A missing target skips the launch entirely, so no dud goes out while a target is mid-respawn. The order is one shot, like the railgun's.
SetInfiniteAmmo
Suspend (or restore) the finite magazine on every weapon of a scoped ship, so a training range or a set-piece is not decided by a reload.
SetInfiniteAmmo((id: "player_spaceship", enabled: true)),
SetInfiniteAmmo((id: "player_spaceship", enabled: false)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | scoped ship root; a dangling id is a lint Error |
enabled |
bool | required | true suspends the magazines, false restores them |
true puts each weapon's finite magazine aside and lets the section fire on
its cooldown alone. false gives the magazine back at the section's own
AUTHORED capacity, full, with the reload re-seeded from the section
prototype - not at whatever count was left when it was suspended. A count
from before the suspension is not a count the run earned either, and a full
magazine is the one state a player and an author can both predict.
Turning it on twice is idempotent, and turning it off on a ship that never had it on does nothing. A weapon authored with no magazine at all was already unlimited and is untouched in both directions.
This is an authoring bypass, not the ordinary reload: the ordinary idle reload
(see Turret reload) spends matching ammunition from
the ship's own inventory; this action never touches
it, in either direction.
RefillAmmo
Top a scoped ship's finite magazines back up without changing how they work.
RefillAmmo((id: "player_spaceship")), // every weapon
RefillAmmo((id: "player_spaceship", section: Some("turret_dorsal"))), // just one
Show explanation
| field | type | default | meaning |
|---|---|---|---|
id |
string | required | scoped ship root; a dangling id is a lint Error |
section |
Option string |
None |
one section's authored id, written Some("..."); omitted refills every weapon on the ship |
Each magazine returns to its authored capacity and any reload in flight is
cleared. Weapons with no finite magazine - and any whose magazine is
currently suspended by SetInfiniteAmmo - are skipped.
An authoring bypass, not the ordinary reload: it fills the magazine directly and spends nothing from the ship's inventory, unlike the ordinary idle reload.
AI constraints
The three actions below retune an AI ship's JUDGEMENT at runtime: how far it
will chase, how far it looks, how close it lets a torpedo come. They accept
ONLY an AI-controller ship - a hull nobody is thinking for has nothing to
retune, so a None or player id is a lint Error.
They are independent of each other and of the helm. Each takes ONE optional payload: set it to install or update the constraint, omit it to put the ship back on the engine default. They are also outranked by a helm order - while one runs, the AI is not flying, so a constraint change has no visible effect until the order lets go.
The spawn-time twins are the AI controller's own leash, engage_range and
pd_range fields (see Spaceship); these are their
runtime mirrors.
SetAILeash
Tether an AI ship's combat to a centre and a radius, or release it. Past the radius the ship breaks off and comes home.
SetAILeash((ship: "picket", leash: Some((center: (0.0, 0.0, -800.0), radius: 2000.0)))),
SetAILeash((ship: "picket")), // let it chase freely
Show explanation
| field | type | default | meaning |
|---|---|---|---|
ship |
string | required | scoped AI-controller ship root |
leash |
Option leash |
None |
Some((center: ..., radius: ...)) installs or updates the tether; omitted RELEASES it |
leash.center |
meters (x, y, z) |
required | the anchor the radius is measured from, in world coordinates |
leash.radius |
meters | required | how far from the anchor combat may go; must be positive |
Widening a leash mid-scenario is how a garrison is let off its post; releasing it entirely turns a picket into a pursuer. The anchor is an authored point, not the ship - moving the ship does not move the tether.
SetAIEngageRange
Change how far an AI ship looks for hostiles, or restore the engine's 4 km default.
SetAIEngageRange((ship: "watchtower", range: Some(16000.0))),
SetAIEngageRange((ship: "watchtower")), // back to the default
Show explanation
| field | type | default | meaning |
|---|---|---|---|
ship |
string | required | scoped AI-controller ship root |
range |
Option meters |
None |
Some(16000.0) installs or updates the range; omitted RESTORES the default; must be positive |
Wide is a long-watch emplacement that wakes for targets parked outside everyone else's detection. Short is a ship that ignores a brawl next door. Narrowing it mid-fight does NOT drop a target it has already acquired.
SetAIPointDefenseRange
Change how close an inbound torpedo gets before an AI ship's guns answer it, or restore the engine's 1.5 km default.
SetAIPointDefenseRange((ship: "escort", range: Some(600.0))),
SetAIPointDefenseRange((ship: "escort")), // back to the default
Show explanation
| field | type | default | meaning |
|---|---|---|---|
ship |
string | required | scoped AI-controller ship root |
range |
Option meters |
None |
Some(600.0) installs or updates the range; omitted RESTORES the default; must be positive |
Short stages the intercept close in, where the player can see it. Past the turret's own ~1.8 km reach it only wastes the opening shots, so authoring wider than the guns can shoot buys nothing.
Variables, timers & debugging
TimerStart
Start a keyed scenario timer. Starting an existing key restarts its deadline.
TimerStart((
key: "orbit_hold",
seconds: Term(Factor(Literal(Number(8.0)))),
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
key |
string | required | scenario-local timer key |
seconds |
numeric expression | required | live, unpaused seconds until OnTimerEnd |
The duration is a numeric expression and must evaluate to a positive finite number. Invalid values log an error and leave an existing timer unchanged.
Timers freeze under pause and clear on retry or teardown. Use an
OnTimerEnd handler with a
Timer filter to react once.
A timer earns its handler when the beat must still ASK something when it
lands - is the wing still alive, which line fits. A run of beats that only
waits for later is a Sequence instead.
TimerCancel
Cancel a running timer. A missing key is a no-op.
TimerCancel((key: "orbit_hold")),
VariableSet
Evaluate an expression against the CURRENT variables and store the result -
the write half of the whole
variable vocabulary. Re-evaluated per event, so
n = n + 1 accumulates.
VariableSet((
key: "asteroids_destroyed",
expression: Add(Factor(Name("asteroids_destroyed")), Term(Factor(Literal(Number(1.0))))),
)),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
key |
string | required | mutable variable to write (overwrites); writing a watched variable is a lint ERROR |
expression |
expression node | required | see the expression grammar; an evaluation error skips the write |
In the in-game editor the expression is not a text field: the action opens a
Value page under its Key, a row per node, exactly as an expression filter
opens its condition. See
the typed form.
DebugMessage
Log a line (debug builds; run with --features dev while iterating). No
game effect - sprinkle these to watch handlers fire.
DebugMessage((message: "gate 3 armed")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
message |
string | required | the log line |
Camera & photo mode
SetCamera
Pin the scenario camera at a pose (drops free-fly control; the pose is re-enforced every frame).
SetCamera((position: (0.0, 300.0, 800.0), look_at: (0.0, 0.0, 0.0))),
// The same pose as a MOVE rather than a cut.
SetCamera((position: (0.0, 300.0, 800.0), look_at: (0.0, 0.0, 0.0), blend: Some((seconds: 4.0)))),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
position |
3-tuple | required | world camera position, meters |
look_at |
3-tuple | required | world point to face, meters (up is +Y) |
blend |
Option blend |
None |
omitted is a CUT; Some((seconds: 4.0)) moves the camera there over four seconds |
Blends. A blend rides on top of the pose, so a blend onto an anchored shot
keeps tracking its subject all the way in. seconds is how long the move takes;
easing is how those seconds are spent - omitted is Smooth, and Linear is
the special case that wants to look like a rig on a rail
(Some((seconds: 4.0, easing: Linear))). A pose authored as a cut also REMOVES
any blend still running, or the cut would be swallowed by the move it was meant
to interrupt.
Part of the screenshot/photo surface; no scenario camera present is a warn no-op.
SetCameraAnchor
Ride the camera on a scenario object at a fixed offset, facing what you name. The shot follows the object every frame, so a cinematic can hold the player's own ship in frame while they are still flying it.
// Over the player's shoulder, watching the carrier come apart.
SetCameraAnchor((anchor: "cutter", offset: (140.0, 55.0, -195.0), frame: World, look_at: Point((-1000.0, 0.0, 2500.0)))),
// Riding the hull, watching whatever is coming.
SetCameraAnchor((anchor: "cutter", offset: (-165.0, 30.0, 70.0), look_at: Object("warship"))),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
anchor |
object id | required | the scoped object the camera rides |
offset |
3-tuple | required | where the camera sits relative to it, meters |
frame |
Local | World |
Local |
whether offset turns with the anchor's hull or stays on world axes |
look_at |
Anchor | Point((x,y,z)) | Object("id") |
Anchor |
what the shot faces |
blend |
Option blend |
None |
omitted is a CUT; see SetCamera for the move |
Local composes the same shot whatever heading the anchor is on - an
over-the-shoulder chase. World composes the same shot whatever the anchor is
DOING, which is what you want while the player is still flying: their heading
is not yours to choose.
look_at: Object follows a live object and falls back to the anchor if that
object dies mid-shot; look_at: Point is fixed world space and survives
whatever was standing there. Frame something that is about to be destroyed with
Point, not Object.
Camera authority only. It never steers or stops the ship it rides and does not
change player input authority. Like SetCamera it drops free-fly control and
re-enforces the pose every frame; unlike SetCamera there is something to hand
back to, so pair it with ReleaseCamera. Pair a staged shot
that must own gameplay input with SuspendPlayerControl
and explicitly resume when the shot returns control. An anchor id that names
nothing warns and leaves the camera alone.
ReleaseCamera
Drop every scripted camera override and hand the view back to the player's normal chase rig.
ReleaseCamera(()),
Show explanation
No fields. Takes off both SetCamera's fixed pose and
SetCameraAnchor's ride, whichever is on. The chase rig
never stopped solving underneath, so the shot ends and the player is looking
out of their own ship again - there is no restore pose to author.
Harmless when nothing is pinned. This is camera-only: it does not implicitly resume player control.
SuspendPlayerControl
Block human flight, look, targeting, combat-stance, and weapon input while the simulation and scripted scene continue.
SuspendPlayerControl(()),
Show explanation
No fields. The action immediately clears held burn, RCS, rotation, radar, combat stance, thruster, turret, torpedo, and railgun intent so an input pressed before the cut cannot remain latched. Pause, menu, and an explicitly always-live cinematic-skip binding remain available. Physics, timers, AI, scripted ships, weapons already in flight, and camera actions continue.
Repeated suspension is harmless. Scenario teardown always restores control,
but authored content should still pair each interval with
ResumePlayerControl.
ResumePlayerControl
Restore human gameplay input after an explicit suspension.
ResumePlayerControl(()),
Show explanation
No fields. Repeated resume is harmless. This is input-only: pair it with
ReleaseCamera when returning both view and control.
Screenshot
Capture the primary window to a PNG. A dev tool - pair SetCamera + settle
frames + Screenshot to script a framed shot.
Screenshot((path: "shots/my_scene.png")),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
path |
string | required | output PNG path |
Relative paths land under the NOVA_CAPTURE_DIR env var when set; parent
directories are created.
SetSkybox
Swap the scenario's skybox mid-scenario.
SetSkybox((cubemap: "self://textures/nebula.png", brightness: Some(700.0))),
Show explanation
| field | type | default | meaning |
|---|---|---|---|
cubemap |
asset ref | required | the new cubemap path ("self://textures/nebula.png") |
brightness |
Option number |
None |
multiplier; None keeps the current brightness (initial scenario default 1000) |
The install is deferred until the new image has loaded; a failed load leaves the sky unchanged (warned).