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 nosection_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 orlootable: trueone, 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, movingcreditsbetween 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_rangestill 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
OnExitfor 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_separationunderScatterObjects). - 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.