Create / Modding reference / Scenario objects

Scenario objects

Everything a scenario can place in the world. An object is spawned by SpawnScenarioObject (or in bulk by ScatterObjects): a shared base block plus a kind that picks one of the SIX kinds below. Every object gets the base's id, name and pose, is scenario-scoped (teardown removes it), and carries a type name the type_name filters match:

kind type name body what it is
Anchor "anchor" static invisible authored gravity well (framing/orbit target)
Asteroid "asteroid" dynamic destructible rock, always mobile and gravity-affected
Planet "planet" static a seeded WORLD: biomes, terrain, always a gravity well
Spaceship "spaceship" dynamic a multi-section ship, player- or AI-flown
Beacon "beacon" static lockable nav marker with a HUD chip
Light "light" static the scene's own lighting

(Trigger AREAS are spawned by the CreateScenarioArea action rather than as an object kind - and a beacon can be its own area, below.)

Anchor

An invisible authored point that publishes a gravity well with an AUTHORED radius: no mesh, no collider, and no geometric extent for AI obstacle avoidance to steer around. Use it where a contract needs a position plus well geometry but the scene does not want a rock there - an orbit directive's target, or a real gravity source with no body. Because the radius is authored (a planet's is derived from its generated mesh), everything reading the well sees the same geometry on every load. An anchor is a STATIC well source, same as a planet: nothing moves it, and nothing but a planet or an anchor raises a well at all - every asteroid and ship is mobile and only ever responds to one.

field type default meaning
body_radius number required the well's published body radius, meters
mass Option number None well STRENGTH: the gravitational parameter mu, same dial as a planet's mass. NOT an SI mass and not a length - it carries a length cubed over a time squared, so it stays an engine number while the radius beside it is metric. What comes out of the pair IS metric: soi = 20 * sqrt(mass) meters of reach. None = a zero-strength well: it frames and anchors but never pulls
SpawnScenarioObject((
    base: (id: "patrol_anchor", name: "Patrol Anchor", position: (0.0, 0.0, 0.0), rotation: (0.0, 0.0, 0.0, 1.0)),
    kind: Anchor((
        body_radius: 800.0,
        mass: Some(30000.0),
    )),
)),

An anchor is indestructible (there is nothing to hit) and static; it never fires destruction events.

Asteroid

A noise-generated destructible rock. radius drives the mesh, collider and radar signature together. Every asteroid can be carved and destroyed. An asteroid never sources a gravity well - only a planet or an anchor does - but it is always a mobile, dynamic body, so it always FEELS one it is near. A body that must last the whole scenario is a planet.

field type default meaning
radius number required nominal radius in meters, and the rock's DURABILITY - see below. The true mesh extent reaches up to 6x this (matters for min_separation)
initial_velocity 3-tuple required explicit initial motion in meters per second, (x, y, z). (0.0, 0.0, 0.0) is an authored stationary start, not immunity to gravity - a rock with no velocity inside a well's reach falls in like anything else
texture asset ref required the fine crevice GRAIN the kind modulates, not the rock's colour (dep://base/textures/asteroid.png is the stock one)
kind string required the rock's KIND: "rock", "metal", "ice", "carbon" or "plain". It decides how the rock LOOKS - see below. There is no default and no fallback
destroy_sound Option asset ref None played on destruction (Some("dep://base/sounds/destroy_rock.wav")); omitted = silent
lock_signature Option number None radar signature override, meters; None = 100 m plus half the rock's TRUE geometric radius (the meshed extent, not the nominal radius above), so a pebble is a close-range contact and a belt body a landmark. Lock range is thirty times the signature. Separately, and whatever the signature says, EVERY asteroid blocks radar: nothing behind it can be locked or detected while it is on the line - see line of sight
seed Option number None silhouette seed. Some pins the generated shape (and the derived geometric extent) across runs; None derives one from the object's own id, so a rock differs from its neighbours but keeps its shape on every load. ScatterObjects fills it deterministically from its own seed
SpawnScenarioObject((
    base: (id: "boulder", name: "Boulder", position: (2500.0, 0.0, 0.0), rotation: (0.0, 0.0, 0.0, 1.0)),
    kind: Asteroid((
        radius: 200.0,
        initial_velocity: (0.0, 0.0, 0.0),
        texture: "dep://base/textures/asteroid.png",
        kind: "rock",
    )),
)),

What a rock is made of

kind is the rock's KIND, and it is REQUIRED. It answers one question: how the body is SHADED. It does NOT decide what the rock sounds like when something hits it - every asteroid is stone to the engine's impact voice, whatever it is made of, and only the damage type changes the bang.

kind reads as
"rock" ordinary stone: warm tan banded with cool slate
"metal" nickel-iron: cold, metallic, bright seams
"ice" water ice: pale blue, glossy, crackled through
"carbon" carbonaceous: near-black and matte
"plain" the control - the texture with nothing done to it

The kind supplies the palette, the large-scale character and the specular; the texture supplies the fine crevice grain the kind modulates. They are two different questions, so a mod keeps its own rock texture and still picks a kind.

There is no default and no fallback. A rock that does not say what it is made of fails to load, and one that names a kind nobody ships is a content lint error, and the scenario refuses to start. A body this big in the frame does not get to be a shrug.

Migrating an older file: the field was called material: in 0.13.x - rename it to kind:, and nothing else about it changed. A file written for 0.12.0 or earlier has no such line at all and needs one on every Asteroid((..)); kind: "rock" is what those files were drawn as, so it is the mechanical migration, and picking a kind per rock is the point of the field. A field scattered by ScatterObjects states its kinds once, in asteroid_kinds, and the scatter writes them into every copy.

A normal asteroid has no health pool. Hits remove signed geometry and severed pieces become debris. When no viable connected solid remains, destruction fires OnDestroyed with the rock's id and "asteroid".

Sizing a rock you want shot

radius IS the durability, so it is the only knob, and it is CUBIC. Material costs 8 hit points per 1,000 m3 - 125 m3 to the hit point - absolutely, on every body, whatever its size, so doubling a radius multiplies the time to break it by about eight. Read that price as a VOLUME and never as a length: it says how much material one hit point takes off, and it is the one figure here that does not scale the way a distance does.

The stock kinetic PDC deals 4.0 per round at 100 rounds a second, so held fire eats 50,000 m3 a second. A rock authored at radius: 30 is about 2.4 minutes of that, one at radius: 3 a couple of seconds. Both read against the MESH rather than the nominal figure - the noise silhouette reaches several times the authored radius, and that swell is the solid the guns have to chew.

Author the size you want the fight to take. The shipped tutorial targets are radius: 2.5 and 3.0 for exactly this reason: small size, not hidden health, is what keeps a target brief now that there is no health field to turn down.

Planet

A world. Pick a TYPE and a SEED and you get a body with its own terrain, palette and polar cap - mountains, basins, seas where the type has them. Nothing is textured and nothing is authored band by band: the type decides what kinds of ground the world can have, and the seed decides which of them it got and where.

Use this for anything meant to read as somewhere. An asteroid with a big radius is a rock the size of a planet, and it looks like one.

A planet can never be destroyed. Weapons fire leaves it alone, and its gravity well lasts the whole scenario.

field type default meaning
planet_type type name required BarrenRock, DustWorld, IceWorld, Volcanic, Greenhouse or Temperate. A name outside that list is a LOAD ERROR, not a fallback
radius number required MEAN radius in meters, and the body's real size. Unlike a rock's nominal radius, the surface stands only 1 + relief off this - a few percent - so this is very nearly what everything measures from
seed number required which world of that type: the biome in every band, the cap latitude, the palette tint and the terrain itself. Required on purpose - a landmark cannot be a body the engine picked for you
mass number required well STRENGTH, exactly as on an anchor: mass = (soi / 20)^2 for an soi in meters. REQUIRED: every planet is a static gravity well, at any radius - there is no size threshold and no default to fall back to
relief Option number None how far the highest ground stands above the mean radius, in meters. None = the type's own (2% of the radius on a hazy greenhouse, 6% on a volcanic world). Must be positive and smaller than the radius
sea_level Option number None where the surface flattens into sea, as a fraction 0-1 of the height range. Some(0.0) drains a sea; None = the type's own (only IceWorld and Temperate have one)
lock_signature Option number None radar signature override in meters; None = ten times the outer surface radius (radius * (1 + relief)), which makes any world a landmark from anywhere a scanner reaches. A planet also blocks radar over its whole sphere - the largest piece of cover you can place - see line of sight
SpawnScenarioObject((
    base: (id: "planetoid", name: "Planetoid", position: (2500.0, 0.0, 0.0), rotation: (0.0, 0.0, 0.0, 1.0)),
    kind: Planet((
        planet_type: DustWorld,
        radius: 950.0,
        seed: 7,
        mass: 27000.0,
    )),
)),

Sizing a planet

Author the size you want the body to BE. A rock's radius is a designation its noise mesh reaches three to six times past; a planet's is the real thing, and the geometric surface every rule reads - the gravity well's clamp, the sphere of influence, the ring OrbitShip flies, a GOTO standoff - is radius * (1 + relief).

That also means porting old content is not a copy of the number. An asteroid with radius: 200.0 draws a body about 1 km across; written as a planet it wants radius: 950.0, not 200.0, or every distance around it collapses by about five.

The MESH follows the radius too: a world is built to a 20 m facet whatever its size, so a small planetoid is no longer paying for a big world's triangles and a big one is round. Detail is capped, so a body past about 1.4 km of radius is meshed coarser than 20 m rather than without limit.

Picking a type

Each type is a palette and a terrain character, not a single look - a seed moves the biomes, the cap and the tint, so two DustWorlds are two worlds.

type reads as has a sea
BarrenRock airless grey stone, dark mare against pale highland no
DustWorld ochre and rust plains under thin polar frost no
IceWorld blue-white sheets and frost peaks yes
Volcanic black basalt cut by glowing fissures no
Greenhouse pale choking haze, almost no silhouette no
Temperate deep sea, sand shelves, green continents, ice cap yes

Spaceship

A spawn of a SHIP: where it sits, who flies it, which side it is on. What it IS - its section layout, its cladding - is a ship, named here by id or authored inline.

field type default meaning
design design source required Prototype(id: "block_gunship") names a ship design by id, with this spawn's own section_patches over it; Inline((..)) carries a one-off design (below)
controller controller required who flies it (below)
initial_velocity 3-tuple required explicit initial motion in meters per second, (x, y, z). (0.0, 0.0, 0.0) is an authored stationary start, not immunity to gravity - a ship with no velocity inside a well's reach falls in like anything else
allegiance Option side None side override, strict RON Some(Neutral). Omitted = the controller default: Player ships fight for the player, AI ships are hostile
capabilities capability set all on what this spawn is PERMITTED to do (below). Omit it for a ship that can do everything
inventory stock required what the ship carries at spawn (below)
lootable bool required true lets a ship docked to this one Take from its inventory although it was never neutralized: a derelict. false allows Take only once it is neutralized. Give is always allowed
credits whole number required the credits the ship holds at spawn. A docked player Buys from and Sells to any ship that is neither neutralized nor lootable, paid from these real credits. Base content gives a live ship 2000 and a derelict 0

design: Inline((..)) carries the same fields a ship design does

  • sections, integrity, presentation. Author one for a genuine one-off (a scripted battery that is a single torpedo tube); anything a second scenario would spawn belongs in the ship catalog. An inline design takes no section_patches: it is already this spawn's own, so anything a patch would say it can simply say.

Capabilities

What the ship may do, on the SHIP, not on its flight computer. Every field is true when omitted, and the whole block is omitted for a ship that can do everything. Losing the controller section takes the hull's steering, never its permissions.

field default meaning
stop_enabled true STOP: flip retrograde and burn to rest
goto_enabled true GOTO: fly to the nav lock and come to rest off it
orbit_enabled true ORBIT: circularize inside a gravity well
lock_enabled true LOCK: the radar gesture and the combat lock it makes
rcs_enabled true RCS: the torque-free fine-adjust push
point_defense_enabled true the turrets engaging incoming ordnance on their own
dock_enabled true DOCK: clamping the hull to the ship it has locked, port to port
// A cadet trainer: guns on the mount, nothing else handed over yet.
capabilities: (
    stop_enabled: false,
    goto_enabled: false,
    orbit_enabled: false,
    lock_enabled: false,
    rcs_enabled: false,
),

Runtime mirror: the SetShipCapability* actions, which is how a tutorial hands them over one lesson at a time. dock_enabled is authored at spawn only; it has no runtime action yet.

Inventory

What the ship carries at spawn: a map from item to stack count. The field is required; write inventory: {} for an empty hold. A count of 0 or the same item listed twice fails lint and load. The design sets the hold: 100 kg per hull section. Stock heavier than the hold fails lint, then load. Stock shows in the TAB interface's Inventory pane. A Ship pane repair spends hull plates, and every weapon's idle reload draws its matching round from this same stock

  • see Turret reload. A docked player can Give items to any ship and Take them from a neutralized or lootable: true one, never past the receiving ship's hold. The player can also Buy items from any other docked ship at the item's ask and Sell to it at the item's bid, moving credits between the two ships at once. A trade takes only stock the seller carries and credits the buyer holds; nothing restocks. An undocked player with a cargo intake can Jettison items in canisters and take canisters back in. Nothing is saved: stock and credits return to their authored values when the scenario loads again.
Item Mass Category Ask Bid
HullPlate 10 kg Repair 40 30
PdcRound 0.2 kg Ammo 4 3
RailSlug 20 kg Ammo 40 30
Torpedo 150 kg Ammo 400 300
StoneOre 10 kg Raw 4 3
IronOre 10 kg Raw 16 12
WaterIce 10 kg Raw 12 9
CarbonOre 10 kg Raw 8 6
Rations 2 kg Food 8 6
SalvagedParts 25 kg Parts 120 90

Ask and Bid are credits for one item, the same on every ship.

PdcRound feeds both Kinetic and Pierce turret mounts - the mount decides the round's damage type, not the item. Torpedo feeds every torpedo bay type, Serpent and Lance alike - the bay decides the flight.

// The open world's derelict: nobody aboard, free to loot.
controller: None,
allegiance: Some(Neutral),
inventory: {
    HullPlate: 8,
},
lootable: true,
credits: 0,

Section patches

section_patches tunes named sections of a catalog design for THIS spawn, keyed by the design's own section ids. A section id the design does not carry is a lint error, and so is a patch that argues with the section's kind.

field type default meaning
position 3-tuple inherit move the section to this mount cell
rotation 4-tuple inherit turn the section to this mount rotation
config section patch inherit what this spawn changes about the section's config (the patch)

An abbreviated player ship:

SpawnScenarioObject((
    base: (
        id: "player_spaceship",
        name: "Player Ship",
        position: (0.0, 0.0, 0.0),
        rotation: (0.0, 0.0, 0.0, 1.0),
    ),
    kind: Spaceship((
        controller: Player((
            input_mapping: {
                "turret_port": [Mouse(Left)],
            },
        )),
        initial_velocity: (0.0, 0.0, 0.0),
        // The shipped patrol gunship, by id, with this spawn's own flight
        // computer hardened; every other gunship is untouched.
        design: Prototype(
            id: "block_gunship",
            section_patches: {
                "bridge": (config: (health: Some(500.0))),
            },
        ),
        inventory: {},
        lootable: false,
        credits: 2000,
    )),
)),

A one-off design, authored inline:

kind: Spaceship((
    controller: None,
    initial_velocity: (0.0, 0.0, 0.0),
    design: Inline((
        sections: [
            (
                id: "bay",
                position: (0.0, 0.0, 0.0),
                rotation: (0.0, 0.0, 0.0, 1.0),
                source: Prototype(id: "torpedo_section"),
            ),
        ],
    )),
    inventory: {},
    lootable: false,
    credits: 2000,
)),

An abbreviated AI ship:

SpawnScenarioObject((
    base: (
        id: "raider_1",
        name: "Picket",
        position: (0.0, 0.0, -3000.0),
        rotation: (0.0, 0.0, 0.0, 1.0),
    ),
    kind: Spaceship((
        controller: AI((
            patrol: [(0.0, 0.0, -3000.0), (800.0, 0.0, -2200.0)],
            engage_delay: Some(8.0),
        )),
        initial_velocity: (0.0, 0.0, 0.0),
        design: Prototype(id: "block_picket"),
        inventory: {},
        lootable: false,
        credits: 2000,
    )),
)),

A ship id nothing authored spawns an empty root and logs an error rather than crashing, so a missing dependency is visible instead of fatal.

The controller

variant meaning
None nobody drives; the ship station-keeps
Player((..)) human-driven
AI((..)) bot-driven

Player((..)) fields:

field type default meaning
input_mapping map {} per-SECTION bindings, keyed by section id: { "turret_port": [ Mouse(Left) ] }. Values are Keyboard(<KeyCode>) / Mouse(<MouseButton>) / Gamepad(<GamepadButton>) - modifier-free buttons only. Every Mining section on the ship needs its own non-empty entry, and no two of them may share a button: lint and load refuse the ship

Player((..)) is STRICT: a key that is not in that table is a load error naming the key, not a key quietly dropped.

AI((..)) fields:

field type default meaning
patrol list of 3-tuples [] waypoint loop in world METERS while nothing hostile is detected; empty = station-keep. Legs blocked by a solid body are flown around automatically - the clearance is the body's geometric radius plus this ship's own live hull radius plus avoid_margin - so routes need not measure every rock. Nav beacons are marks to fly TO, never obstacles, so a route can be its own beacons
patrol_stops list of numbers [] seconds this ship station-keeps at each patrol waypoint on arrival, before flying the next leg, in waypoint order; [] = no stops. A list of another length than patrol, or a stop that is negative or not finite, is a lint error and a spawn panic
orbit Option well target None gravity well to orbit passively: Some(Authored("planetoid")) names a well this scenario spawned (a dangling id is a lint Error), Some(NearestToShip) takes, among loaded wells with a stable ring, the one whose gravity owns the ship when the orbit engages, else the nearest, and picks again when the ship comes back from a fight. With no match the ship drifts and tries again until one loads. Precedence: orbit > patrol > idle
sensor_range Option number None scanner-reach override in meters: how far this ship can hear anything at all, instead of the default 20,000 m. It is a CEILING over the signature model, not a replacement for it - a target is seen inside min(this, its own signature x 30) - so authoring it wide does not make a quiet target loud. Some(0.0) is a ship that sees nothing (a decoy, a derelict that must never react), and so is any hull whose last flight computer is dead, whatever is authored here. Not engage_range: this decides whether the ship CAN see something, that one decides whether it leaves its routine for it
engage_range Option number None hostile-detection override in meters: a passive ship leaves its routine for a hostile inside this range instead of the default 4,000 m. Wide = a long-watch emplacement that wakes for targets nothing else detects; short = a ship that ignores a nearby brawl. Range is the CEILING, not the guarantee: detection is line-of-sight gated, so a wide emplacement parked behind a planetoid never wakes - see line of sight
standoff_clearance Option number None combat standoff override in meters: the clear space this ship wants between its own hull and its target's while it fights, instead of the default 1,000 m. A FACE distance - the engine adds both ships' live hull radii to it - so the same number means the same thing whether the fight is two skiffs or two carriers. Author what a player should SEE between the two skins. Some(0.0) is legal and closes until the hulls meet: a boarder, a rammer. Not where the guns open, which is the weapon's own reach, and not arrival_standoff, which is navigation
pd_range Option number None point-defense override in meters: the guns hold fire until an inbound hostile torpedo is inside this range instead of the default 1,500 m. Short = staged close-in intercepts; past the turret's ~1,800 m reach it just wastes the opening shots
waypoint_slack Option number None patrol arrival slack override in meters on top of where the leg comes to rest (hull + arrival standoff); the default is 250 m. Small = the ship turns onto the next leg closer to each waypoint. The gate is measured from the same point the leg parks, so the slack has only the arrival's own terminal drift left to cover - author small, not zero
avoid_margin Option number None patrol avoidance margin override in meters: the daylight this ship wants between its own hull and a sized body's, instead of the default 200 m. A FACE distance - the engine adds this ship's live hull radius - so the same number is the same visible gap for a skiff and for a carrier. Some(0.0) is legal and passes on the hull's own skin: a miner who knows the belt. Only the patrol routine avoids; a ship in combat flies its fight
arrival_standoff Option number None the navigation margin in meters between this ship's own hull and a GOTO goal's surface, instead of the engine's default 500 m. Some(0.0) is legal and parks the hull's face on the mark. Pair a small margin with a small waypoint_slack so a nav ship visibly REACHES its marks (the patrol turns at hull + margin + slack)
leash Option number None territorial tether radius in meters; combat breaks off beyond it; None = chases freely
engage_delay Option number None arrival grace in seconds: flies its passive routine and refuses to engage until it elapses; being SHOT ends the grace instantly and permanently. The telegraphed-arrival tool
non_combatant bool false this ARMED ship flies itself but never fights: it patrols, orbits, avoids and station-keeps as any AI ship does, and never acquires a target or pulls a trigger
order_interruption Option policy None when a scenario helm order may be broken off to fight: Some(OnHostileContact) while a hostile is acquired, Some(OnDamage) while it is being shot. Omitted = never - the order runs to its end

An UNARMED AI ship (no turret or torpedo section) is automatically a NON-COMBATANT: it flies its routine and never acquires, chases or shoots - no field to set. Keep its allegiance on the side you want hunted and it becomes something to defend (a convoy you have to keep alive).

non_combatant: true asks for the same standing-down on an ARMED hull - a military escort holding formation through a scene it takes no part in. Do not try to fake it with a long engage_delay or a tiny engage_range: both are timers and distances that eventually expire or are crossed, so the ship opens fire in the middle of a beat that assumed it would not. This is a standing statement about the hull.

For a ship the SCENARIO drives shot by shot, author None and use the helm orders and forced fire actions. The two ends cover the whole space between them: a non-combatant flies itself and never shoots, a None ship does exactly and only what it is told.

The helm orders also work on an AI ship, which is the MIDDLE of that space: a bot that thinks for itself until a mission tells it where to be, then thinks for itself again. order_interruption is how much the fighting is allowed to interrupt the errand - and the default is not at all, so an order given to an AI ship lands exactly as it would on a None hull unless the author says otherwise.

Allegiance values: Player / Enemy / Neutral. Player and Enemy are mutually hostile; Neutral relates neutrally to everyone (stray blast damage still hurts it). Runtime flip: SetAllegiance.

The sections list

Each entry places one section in continuous ship-root space:

field type default meaning
id string required scenario-local section id; keys input_mapping (shipped hulls name their specials, such as "pdc_forward_port")
position 3-tuple required continuous offset from the ship root, in BUILD CELLS (one cell is 10 m) - the one authored position that is not metric
rotation 4-tuple required rotation relative to the root; structural link points rotate with the section
source source required Prototype(id: "<id>") - a catalog id, the compact reusable form, with an optional patch over it - or Inline((..)) with a full section config (Ship sections for mods)

A Prototype reference may carry a patch: a typed, curated delta over the section it resolves to. An omitted field INHERITS the prototype's value, Some(value) replaces it, and for a nullable field Some(None) clears it. An Inline source takes no patch - it is the config, so anything a patch would say it simply says.

field type default meaning
health Option number inherit the section's starting health (current and max)
kind Option kind patch inherit the kind-specific delta, tagged with the section's own kind: Turret((..)), Torpedo((..)), Railgun((..)), Thruster((..)), Controller((..)), Hull(()). A kind that argues with the prototype is a lint error

What each kind patch reaches:

kind fields
Thruster magnitude
Controller steering_lag, max_torque
Turret muzzle_speed, projectile_lifetime, bullet_damage, bullet_kind, ammunition, reload, muzzles (per-barrel fire_rate, keyed by muzzle id)
Torpedo fire_rate, spawner_speed, projectile_lifetime, arm_time, arm_distance, nav_constant, blast_radius, blast_damage, ammunition, reload
Railgun charge_seconds, slug_speed, slug_damage, slug_power, rake_radius, slug_lifetime, recoil_impulse, ammunition, reload
Hull nothing of its own - a hull's health is the common health above

Art, colliders, sockets, link points, the turret joint tree, the section kind and the source itself stay the prototype's. A patch that could move them would make the prototype meaningless.

The editor writes these patches for you. A part placed from the parts gallery names the catalog part it came from, and tuning a field on it marks the row: the build keeps the reference plus the delta, not a copy. The mark carries a reset control that DROPS that field from the patch, so the part's own value reaches the ship again - and a later change to the part reaches every ship that did not tune the field. A gun with more than one barrel shows a group of rows per muzzle id, which is what muzzles is keyed by. An edit no patch can say falls back to an Inline copy.

// A catalog mount that carries one dry magazine on this build.
(id: "turret_port", position: (1.2, 0.7, 0.5), rotation: (0.0, 0.0, 0.0, 1.0),
 source: Prototype(
     id: "pdc_kinetic_turret_section",
     patch: (
         health: Some(90.0),
         kind: Some(Turret((
             ammunition: Some(Limited(200)),
             reload: Some(Disabled),
             muzzles: { "main": (fire_rate: Some(80.0)) },
         ))),
     ),
 )),

Ship structure is linted from authoritative link-point mates. A multi-section ship must form one graph. Collider AABB overlap is an Error unless the two sections directly mate, which permits intentional interlocking parts while still catching accidental embedding.

Beacon

A static, lockable, blinking nav marker with an automatic HUD chip (label, live distance, edge-clamped direction cue).

field type default meaning
label string required HUD chip text ("BEACON 1")
radius number required visual orb radius, meters
color color required orb + emissive tint, tagged: Srgba((red: 0.3, green: 0.9, blue: 1.0, alpha: 1.0))
area_radius Option number None when set, the beacon IS its own trigger area of this radius in meters - OnEnter/OnExit fire under the beacon's id, no CreateScenarioArea needed. Entering it moves no cargo
lock_signature Option number None radar signature override in meters; the default 400 m gives a lock range of about 12 km - author bigger for longer GOTO legs
SpawnScenarioObject((
    base: (id: "beacon_1", name: "BEACON 1", position: (0.0, 0.0, -3500.0), rotation: (0.0, 0.0, 0.0, 1.0)),
    kind: Beacon((
        label: "BEACON 1",
        radius: 20.0,
        color: Srgba((red: 0.3, green: 0.9, blue: 1.0, alpha: 1.0)),
        area_radius: Some(700.0),
    )),
)),

Light

The scene's own lighting - and it is load-bearing: the engine spawns NO default light, so a scenario with no Light object renders black. Light takes one of two methods as an inner enum with NAMED fields in single parens (not the newtype double-paren shape).

Directional - a sun: parallel rays, direction only. The key/rim/fill workhorse.

field type default meaning
illuminance number required lux (the shipped key lights run ~11000)
color color required tagged Srgba((..))
shadows bool required shadow casting; convention: exactly ONE caster per scene
aim Option 3-tuple None point the light AT this world position in meters, ignoring base.rotation (hand-authoring an aim quaternion is impractical); None uses the rotation

Point - a positional lamp with falloff: a star, a floodlight, a nebula glow.

field type default meaning
intensity number required lumens; needs tuning by eye against your scene scale (~2.5M for a yard lamp 2 km out)
range number required contribution cutoff distance, meters (the shipped lamps run 4,000)
radius number required source radius in meters (softens the terminator; the shipped lamps run 120)
color color required tagged Srgba((..))
shadows bool required shadow casting
SpawnScenarioObject((
    base: (id: "key", name: "Key Light", position: (-600.0, 500.0, 600.0), rotation: (0.0, 0.0, 0.0, 1.0)),
    kind: Light(Directional(
        illuminance: 11000.0,
        color: Srgba((red: 1.0, green: 0.96, blue: 0.9, alpha: 1.0)),
        shadows: true,
        aim: Some((0.0, 0.0, 0.0)),
    )),
)),

The shipped scenes all use the same three-point rig - an 11000 lux warm key (the only shadow caster), a 16000 lux cold rim from behind, and a 2600 lux cool fill from the shadow side. Copy the full light blocks from assets/mods/example/example.content.ron.

Radar line of sight

Asteroids and planets are the two object kinds that block radar. While one sits on the line between a sensor and a body, that body is not there: it cannot be locked, it does not raise a threat arrow, and an AI ship does not detect it. Nothing else in the catalog occludes - beacons, lights, trigger areas and other ships are all transparent to the sweep.

This makes cover a thing you PLACE, and it has consequences a range number does not warn you about:

  • A picket with a wide engage_range still needs the line. Parked behind a planetoid, it watches nothing.
  • A belt scattered across an approach lane will make a hostile engage, lose the pick and go passive, then engage again as the geometry opens and closes. If a ship looks indecisive, look at what is between it and its target.
  • A rock carves as it is shot, so cover a scene depends on can be shot away.
  • Range is a ceiling on top of sight, not an alternative to it. Both have to pass.

A lock is a radio link, so cover takes the ones already held too: a rock crossing the line drops BOTH of the player's slots, the white travel designation as well as the red combat lock, and neither re-locks by itself when the line clears. A trip the player has already ENGAGED is the exception - the autopilot owns its destination once it is flying - so a rock is still not a way to cancel someone's burn.

Traps for the unwary

  • A despawned object fires no OnExit for itself, and a beacon area dies with its object.
  • Dynamic bodies (asteroids, ships) spawned overlapping shove apart violently on the first physics step - keep spawns separated (see min_separation under ScatterObjects).
  • Every object id must be unique among live scoped entities; duplicate spawn ids in one handler are a lint Error.
  • Ships are verbose; scatter is seeded. If you are typing a 400-line ship by hand, stop and reference prototypes instead.