Create / Mod files / Ship sections for mods
Ship sections for mods
A Section is a reusable ship part defined in a mod's *.content.ron file.
Create a new id to add a part to the editor palette, or reuse a base id to
replace that part everywhere. The five available kinds are Hull, Thruster,
Controller, Turret, and Torpedo.
Start with the two working section items in
assets/mods/example/example.content.ron: one replaces
reinforced_hull_section, and one adds example_plated_hull_section. Use the
base content catalog to find prototype
ids and reusable dep://base/ assets. See Mod files for the
bundle structure and Publish a mod when the part is ready
for release.
This page is the field-by-field section reference. For general RON spelling
rules such as double parentheses, Some(...), and asset schemes, see
RON spelling rules.
The Section item
A content file is a list. Each section is one Section((...)) item with a
shared base block and one kind-specific block:
[
Section((
base: (
id: "my_mod_thruster",
name: "Compact Thruster",
description: "A light engine for small ships.",
health: 70.0,
),
kind: Thruster((
magnitude: 1.0,
)),
)),
]
Common fields
| field | type | default | meaning |
|---|---|---|---|
base.id |
string | required | Prototype key used by source: Prototype("<id>"). A new id adds a part; a matching id replaces the earlier part. Prefix new ids with your mod id. |
base.name |
string | required | Display name in the editor palette and ship UI. |
base.description |
string | required | Editor and tooltip description. |
base.health |
number | required | Hit points before the section is destroyed. |
base.impact_sound |
Option asset ref |
None |
Sound played when this section is hit. Omitted means silent. |
base.destroy_sound |
Option asset ref |
None |
Sound played when this section is destroyed. Omitted means silent. |
base.collider |
Option collider |
None |
Physics shape. Omitted means a 1 x 1 x 1 cube. |
base.link_points |
link-point list | [] |
Structural sockets. Multi-section ships must derive one connected graph from their mates. |
base.hide_in_editor |
bool | false |
true hides the prototype from the editor palette. Ships can still reference it. |
base.damage_effects |
effect list | ([Cracks]) |
The damage looks this section wears. Omitted means its surface cracks, which is what every section does unless it says otherwise. |
kind |
section kind | required | Hull((...)), Thruster((...)), Controller((...)), Turret((...)), or Torpedo((...)). |
Collider forms:
collider: Some(Cuboid(size: (1.0, 0.5, 1.5))),
collider: Some(Sphere(radius: 0.5)),
collider: Some(Capsule(radius: 0.4, length: 1.0)),
collider: Some(Cylinder(radius: 0.5, height: 1.0)),
Cuboid.size is the full size on each axis. Capsules and cylinders extend
along local Y. The collider is also what the section WEIGHS: every section runs
at a density of 1, so its mass is the volume of this shape and a bigger box is
a heavier part. There is no density knob - a section is solid ship, and the
render mesh never counts.
An integral cuboid of at least one cell on every axis is also the section's cell
footprint. For example, Cuboid(size: (3.0, 3.0, 2.0)) occupies 18 cells as one
machine. Skin and clearance read every occupied cell. Put mounting sockets at
the centres of the cells on each face that can attach; collider contact alone
never creates structure. Non-integral cuboids and non-cuboid shapes remain
one-cell footprints.
Damage effects
What a section LOOKS like as it takes damage, authored as a list:
damage_effects: ([Cracks, Sparks, Plume]),
| effect | what it does |
|---|---|
Cracks |
The section's surface fractures where it is failing, glows through the cracks when it is critical, and burns out cold when it dies. |
Sparks |
The section throws sparks, faster the worse it is. |
Plume |
The section's exhaust guts and flickers. Thrusters only - it grades the exhaust cone, so a section with none shows nothing. |
Cracks grades in EIGHT steps from painted to burnt, so the first fractures
show once about a fourteenth of the section's own health is gone and every step
after that is a seventh. Stepping it is what keeps a fleet's damage off the
frame rate - a continuous value per section put every section mesh in a draw bin
of its own. Sparks and Plume both start once the section is properly hurt -
past 35 percent of its own health gone - so a scratch does not make a part read
as broken.
Omitting the field means ([Cracks]). Author ([]) for a section that should
never show damage at all - saying "none" and saying nothing are different.
The rule the list is kept honest by: NO SECTION LOSES GEOMETRY. Every effect is a material or a particle, and the only thing that changes a ship's shape is a whole piece leaving - a cladding plate shot off, a section destroyed. Where a section carries expendable material it wears cladding, and the cladding comes off first (see ships).
There was a Carve effect that cut a real crater out of a section's drawn mesh.
It is GONE, and mods authoring it will fail to load. It read well in a gallery
and badly in a fight: reading a solid out of authored art costs 6-15 ms per
mesh, a ship's marks belong to its root, and so a single round anywhere on a
hull turned every drawn mesh under it into a solid in one frame - measured at
325 meshes and 2.0 seconds. Asteroids still carve, because a rock's solid comes
from the same noise its mesh does and never has to be read out of art.
Effects compose freely and none of them changes what the section DOES: a damaged thruster delivers exactly the thrust it authored.
Structural link points
Structural attachment is explicit and independent of the collider. Two points
mate when their transformed positions coincide and their outward unit normals
oppose. The point id is unique within its section and is used for diagnostics
and UI; IDs do not have to match.
Author normals on an AXIS, and positions at face centres, unless the part really needs otherwise. A socket's roll zero is derived from its normal, so two parts that were never drawn together still mate square; an off-axis normal instead tilts everything mated onto it by exactly its own angle. Size is free: a part authored at its own size mates one of any other size - a 0.3 mount sits on a 1.0 hull face - so a small mount does not have to pretend to be a cube.
A one-unit cube normally authors six face-center points. Example pair:
link_points: [
(
id: "positive_x",
position: (0.5, 0.0, 0.0),
normal: (1.0, 0.0, 0.0),
),
(
id: "negative_x",
position: (-0.5, 0.0, 0.0),
normal: (-1.0, 0.0, 0.0),
),
// Add positive_y, negative_y, positive_z, and negative_z as needed.
],
Rules:
- Positions and normals are section-local.
- Normals must be finite, nonzero, and unit length.
- One point cannot mate with multiple points.
- Unused exterior points are valid.
- A multi-section ship must form one connected mate graph.
- Omitted
link_pointsmeans no sockets. Collider contact and one-unit spacing do not provide compatibility behavior. - A section override replaces the complete prototype. Repeat its link points or author new ones; it does not inherit sockets from the replaced section.
Error-level graph findings prevent the scenario from starting.
Link points are also how a part is PLACED. The editor mates the socket nearest the pointer with one of the placed part's own, so a prototype with no sockets can be spawned by a ship but never built by hand, and a part with a single socket only attaches that way round. The builder chooses which of the part's sockets does the mating and how far it is rolled about the mating axis; everything else follows from the pair. A placement is refused - and says why - when the target socket is already mated, when it would leave a socket with two suitors, or when the part would sit inside a section it does not mate with.
Assets and visual transforms
Asset fields use namespaced strings:
self://models/my-part.glb#Scene0- art listed in your own bundle.dep://base/gltf/hull-01.glb#Scene0- art from the base content catalog.
Never use a bare path. Most mesh, sound, and effect fields are optional. Omit them for the built-in visual or silent audio behavior described below.
Hull, thruster, controller, and torpedo sections can move their render mesh without moving the collider:
render_mesh_transform: Some((
position: (0.0, 0.1, 0.0),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (0.5, 0.5, 0.5),
)),
All three fields default independently: omit position for zero, rotation
for identity and scale for the size the art was modelled at. Turrets place
this transform on each joint instead.
scale resizes the ART, not the section. The collider, the link points and the
mass stay exactly as authored, so shrinking a mesh does not shrink what the part
occupies - set the collider and its sockets to match if you want them to agree.
Resizing a whole ASSEMBLY (a turret's joint tree) takes both halves: scale every
joint's mesh AND every joint offset by the same factor, or the parts stay spaced
for the size they used to be.
Hull
HullSectionConfig - passive armor. One optional field:
Section((
base: (
id: "reinforced_hull_section",
name: "Reinforced Hull Section",
description: "A reinforced hull section for spaceships.",
health: 200.0,
),
kind: Hull((
render_mesh: Some("dep://base/gltf/hull-01.glb#Scene0"),
)),
)),
render_mesh(optional) - the hull mesh; omit for a default 1x1x1 cuboid.render_mesh_transform(optional) - visual-only position, rotation and scale; does not move or resize the collider.- every section's
baseblock also takesimpact_sound+destroy_sound(optional) - the sounds a hit on / the destruction of THIS section plays, asset refs like the meshes (dep://base/sounds/impact.wav/dep://base/sounds/explosion.wavare the base voices); an omitted sound is silent. Per-target = per-material: your reinforced hull can clang.
Thruster
ThrusterSectionConfig - forward thrust.
kind: Thruster((
magnitude: 1.0,
)),
magnitude- impulse applied at full throttle on each fixed simulation tick. Larger values accelerate the same ship faster; compare against the base prototypes when balancing.render_mesh(optional) - custom mesh; omit for the default thruster body.render_mesh_transform(optional) - visual-only position, rotation and scale.loop_sound(optional) - the engine hum this thruster contributes to (dep://base/sounds/thruster_loop.wavis the base drone); thrusters sharing a sound share one loop whose volume tracks the loudest ship burning it. An omitted sound hums nothing.exhaust(optional) - custom flame placement and shape.Noneuses the standard cone. WriteSome((...))when fitting exhaust to custom art.
exhaust fields:
| field | type | default | meaning |
|---|---|---|---|
offset |
3-tuple | (0.0, 0.0, 0.3) |
Flame origin relative to the section. |
rotation |
4-tuple | standard rear-facing rotation | Rotates the flame, which is built along local +Y. |
shape.geometry |
Cone or Rect |
Cone |
Round or rectangular flame cross-section. |
shape.width, shape.height |
number | 0.8, 0.8 |
Full rectangular nozzle size; ignored by Cone. |
shape.exhaust_height, shape.exhaust_radius |
number | 0.1, 0.4 |
Outer flame length and base radius. |
shape.exhaust_max |
number | 1.0 |
Outer flame peak intensity. |
shape.exhaust_inner_height, shape.exhaust_inner_radius |
number | 0.05, 0.1 |
Inner core length and radius. |
shape.exhaust_inner_max |
number | 0.5 |
Inner core peak intensity. |
shape.emissive_color, shape.emissive_inner_color |
linear color | cyan, blue | Outer and inner glow colors. |
Every nested exhaust field has a default, so a mod can override only the values
it needs. Copy a semantic engine part from
assets/base/sections/base.content.ron for a complete rectangular example.
Controller
ControllerSectionConfig - the steering PD controller; a ship needs a live one
to fly.
kind: Controller((
steering_lag: 0.5,
max_torque: 1501.0,
)),
steering_lag- approximate time in seconds that the hull trails a continuously moving steering command. Larger values make the computer feel laggier and brake earlier; smaller values track more tightly. The value must be positive and finite. Very small values are allowed but can make the fixed-step controller unstable. This is not a startup delay or total turn time: the computer reacts immediately, and the hull's turn ceiling still limits large turns.max_torque- how hard this computer's reaction wheels twist the hull, in torque units. Every shipped controller carries1501.0. Controllers on one hull ADD: two are twice the torque, with no cap and no stacking curve.render_mesh(optional) - custom mesh; omit for the default body.render_mesh_transform(optional) - visual-only position, rotation and scale.lock_on_sound,lock_off_sound,radar_deny_sound,radar_retarget_sound,safety_on_sound,rcs_loop_sound(all optional) - the computer's radar/lock and weapons-safety feedback ticks, asset refs like the meshes (dep://base/sounds/lock_on.wavetc. for the base cues); an omitted cue is silent. Your ship's computer can have its own voice.
What a hull does with the torque
A hull may mount several controllers, but they do NOT each steer it: the ship derives ONE attitude loop and shares it out. The turn ceiling it derives is never authored:
ceiling = min( sum(max_torque) / I , 78.48 / (r * 10) ) rad/s2
I- the hull's largest principal moment of angular inertia, measured by the physics engine from the live section colliders and their densities.r- the structural arm in world units: the distance from the hull's centre of mass to the outer FACE of its furthest live section.78.48m/s2 is 8 G, the load hull metal takes. One global constant, the same for every ship and every mod.10is metres per world unit.
So max_torque is the only handling number you author, and it binds only on a
hull heavy enough that its computers give up before its metal does. Everything
that ships is on the second term: the hull would tear first, so fitting more
computers buys it no turn rate at all. Size and shape set the rest. A long hull
has a long arm and a low ceiling; a short one is sharp. Author a bigger
max_torque for a capital-scale hull that reads sluggish, not for a small one -
a small one is already at its limit.
Three consequences to author around:
- Damage sharpens a hull. Losing sections shortens
r, which raises the ceiling. A wreck turns harder than it did intact. - A hard turn spends the margin. Write
r_mfor the arm in metres (r * 10). The turning loadalpha * r_mand the centripetal loadomega^2 * r_madd as a vector, and that sum is what must stay under 8 G. So a hull holdssqrt(78.48 / r_m)rad/s indefinitely and has no authority left to tighten past it. - Stacking buys precision, not authority. A stack starts arresting a turn
earlier and lands on the commanded attitude instead of swinging past. That
gain approaches x1.5 from below: x1.25 at two computers, x1.375 at four,
x1.45 at ten. A mixed stack takes its response from the smallest live
steering_lag.
Turret
TurretSectionConfig - an articulated gun that aims with intercept lead and
fires bullets. The mount is an arbitrary tree of joints (root): each joint
sits at an offset from its parent, optionally rotates about an axis (a hinge
the aim solver drives), optionally carries a render_mesh, optionally is a
muzzle (a fire point), and hangs children joints off itself. Today's turret
is one specific tree - base(fixed) -> yaw(axis Y) -> pitch(axis X) ->
barrel(fixed) -> muzzle - but you can build twin barrels, extra hinges, or a
turret whose elevation lives two joints down. The shipped
pdc_kinetic_turret_section is the reference. Its base block is half the
part - a 0.5 mount box with ONE socket on its underside is why the same gun
bolts to any hull face:
base: (
id: "pdc_kinetic_turret_section",
health: 130.0,
collider: Some(Cuboid(size: (0.5, 0.5, 0.5))),
link_points: [(id: "base", position: (0.0, -0.25, 0.0), normal: (0.0, -1.0, 0.0))],
damage_effects: ([Cracks, Sparks]),
// name, description and sounds omitted
),
kind: Turret((
root: (
offset: (0.0, -0.25, 0.0), // base (fixed)
render_mesh_transform: Some((scale: (0.5, 0.5, 0.5))),
children: [(
offset: (0.0, 0.05, 0.0),
axis: Some((0.0, 1.0, 0.0)), // yaw hinge (Y)
render_mesh: Some("dep://base/gltf/turret-yaw-01.glb#Scene0"),
render_mesh_transform: Some((scale: (0.5, 0.5, 0.5))),
children: [(
offset: (0.0, 0.166353, 0.151977),
axis: Some((1.0, 0.0, 0.0)), // pitch hinge (X)
min: Some(-0.17453294), max: Some(1.5707964), // pitch limits
render_mesh: Some("dep://base/gltf/turret-pitch-01.glb#Scene0"),
render_mesh_transform: Some((scale: (0.5, 0.5, 0.5))),
children: [(
offset: (0.0, 0.0642185, -0.0553645), // barrel (fixed)
render_mesh: Some("dep://base/gltf/turret-barrel-01.glb#Scene0"),
render_mesh_transform: Some((scale: (0.5, 0.5, 0.5))),
children: [(
offset: (0.0, 0.0, -0.6), // muzzle (fixed)
muzzle: Some((fire_rate: 100.0)),
)],
)],
)],
)],
),
muzzle_speed: 100.0,
projectile_lifetime: 2.0,
bullet_damage: 4.0,
bullet_kind: Kinetic,
fire_sound: Some("dep://base/sounds/turret_fire.wav"),
dry_fire_sound: Some("dep://base/sounds/dry_fire.wav"),
ammo_capacity: Some(500),
reload: Some((delay: 3.0, amount: 200)),
)),
Per-joint fields (on every root/children node):
offset-Vec3local translation from the parent joint (the section origin forroot), a bare 3-tuple. A joint'schildrenare placed in its ROTATED frame, so they swing with it.axis(optional) - the local hinge axis (a bare 3-tuple like(0.0, 1.0, 0.0)). Omit for a FIXED node (offsets and can still carry a mesh/muzzle, never rotates); set it to make the joint a hinge the aim solver steers. A muzzle's forward is its local-Z; the solver distributes the aim across every hinge above it.speed(optional) - traverse speed in radians per second; omit for the default 180 deg/s (PI). Only meaningful on a hinge (axisset).min,max(optional) - rotation limits in radians for this hinge;Nonefor no limit.render_mesh(optional) - this joint's mesh; omit for a plain default primitive. Shipped turrets author a GLB per visible joint.render_mesh_transform(optional) - re-seats or resizes this joint's render mesh visually without moving the hinge or the collider. It also sizes the DEFAULT primitive an unmeshed joint gets, which is a full unit across - scale it with the rest, or a small turret wears a hull-sized base plate.muzzle(optional) - marks this joint a fire point:Some((fire_rate: N))(rounds per second), plus an optionalmuzzle_effectflash asset ref. A turret aims and fires ALL of its muzzles: hang two off one barrel for a twin PDC, or give each its own arm. Every muzzle fires at its ownfire_ratebut draws from the ONE shared section magazine (ammo_capacity), so a twin barrel empties the same mag twice as fast rather than carrying a pool per gun.children(optional) - joints hanging off this one; omit for a leaf.
Section-wide fields (once, alongside root):
-
fire_sound(optional) - the sound each fired round plays, an asset ref like the meshes (self://a wav your mod ships, ordep://base/sounds/ turret_fire.wavfor the base cue); omit and the turret fires SILENTLY (the base turrets author it, so copy their line if you want the stock sound). Your turret can sound like its own gun. -
dry_fire_sound(optional) - the click when the trigger is pulled on an empty magazine; same asset-ref rules (dep://base/sounds/dry_fire.wavis the base click), omit for a silent dry pull. -
muzzle_speed- projectile launch speed in units per second (shared by all muzzles;fire_rateis per-muzzle, see the joint fields above). -
projectile_lifetime- projectile lifetime in seconds. A turret has no range field:muzzle_speed * projectile_lifetimeIS its reach (the stock PDC reaches 200 units, 2 km). An AI ship holds fire past 90% of that and settles into a fight at ~100 units, so a gun authored with much less reach than that belongs on a player ship - an AI carrying it orbits outside its own range and never fires. -
bullet_damage- authored damage per hit, before the closing-speed curve. It is the SAME number against every section: there is no resistance table, so a damage type never multiplies what a round deals. -
bullet_kind- the damage type of the loaded round (Kinetic,Pierce, orExplosive). A type decides how the round TRAVELS, not how much it deals:Kineticis the punch.bullet_damagedoubles as its budget: it continues only through what it DESTROYS, spending the target's health out of that budget, and stops at anything it fails to kill. Closing speed scales its damage (clamped 0.25x..2.0x).Pierceis the rake. It dealsbullet_damagein full to EVERY section it crosses, alive or dead, undiminished by depth, so its total legitimately exceeds one round's worth. It pays for travel out of a separate power budget: crossing a section costs that section'shealthRATING (not its remaining health), and closing speed divides that cost (clamped 0.5x..3.0x). A rake is bounded by its power and by a hard six-layer cap.Explosiveon a bullet is spent on its first hit. A torpedo blast uses the Explosive pressure rule described below.
Both curves read exactly 1.0 when the round closes at 100 u/s (a stock PDC's
muzzle_speed), so authorbullet_damagefor a station-keeping engagement and speed does the rest. Nothing of any type gets through a collider with no health of its own - an asteroid or a planetoid is a wall. -
projectile_render_mesh(optional) - custom bullet mesh. Omit it and the round is drawn from the built-in set, which is keyed by the FIRED round's damage type rather than by the turret: a Kinetic slug is stubby, a Pierce round a long thin dart, an Explosive shell squat and wide, each in that type's HUD colour. Setting this field overrides all three - one mesh, whatever the turret has loaded. -
ammo_capacity(optional) - magazine size;Nonefires without a limit,Some(n)gives an ammo slot ofnrounds. -
reload(optional) - idle batch reload for the magazine (needsammo_capacity).Some((delay, amount)): every successful shot resets the timer; afterdelayquiet seconds,amountrounds return, clamped to capacity. Batches repeat while the weapon stays idle. An empty trigger pull does not reset the timer.None= a spent magazine stays empty.
Torpedo
TorpedoSectionConfig - a bay that launches guided, proportional-navigation
torpedoes dealing blast damage. The shipped torpedo_section:
kind: Torpedo((
render_mesh: Some("dep://base/gltf/torpedo-bay-01.glb#Scene0"),
spawn_offset: (0.0, 0.0, -2.0),
spawn_rotation: (0.0, 0.0, 0.0, 1.0),
fire_rate: 1.0,
spawner_speed: 8.0,
projectile_lifetime: 100.0,
arm_time: 0.5,
arm_distance: 5.0,
ignition_delay: 0.6,
nav_constant: 3.0,
linear_damping: 0.8,
blast_radius: 30.0,
blast_damage: 750.0,
torpedo_type: (
name: "Serpent",
tint: Srgba((red: 0.95, green: 0.45, blue: 0.1, alpha: 1.0)),
max_speed: 32.0,
weave_angle: 0.44,
weave_rate: 1.4,
),
ammo_capacity: Some(6),
reload: Some((delay: 10.0, amount: 1)),
)),
-
launch_sound(optional) - the sound a departing torpedo plays (dep://base/sounds/torpedo_launch.wavis the base whoosh); omit for a silent launch. -
detonation_sound(optional) - the sound the warhead plays when it blasts; rides the torpedo's own destroy event, so it fires even when a torpedo is shot down. Omit for a silent detonation. -
render_mesh,projectile_render_mesh(both optional) - the bay mesh and the torpedo mesh. Omitprojectile_render_meshand the warhead flies as the built-in coned body, nose along its direction of travel. -
render_mesh_transform(optional) - visual-only bay mesh position, rotation and scale. It does not move the launch point. -
spawn_offset(Vec3),spawn_rotation(Quat, a bare 4-tuple) - where the torpedo leaves the bay, relative to the section. -
fire_rate- launches per second. -
spawner_speed- the ejection charge, in units per second. A torpedo is not fired, it is dropped: this is the cold kick that pushes it clear of the hull, not the speed it flies at. That comes from the drive, once it lights. -
projectile_lifetime- torpedo lifetime in seconds. -
arm_time,arm_distance- the torpedo may detonate only after this many seconds OR this distance from the muzzle (arms on whichever comes first), so it clears the firing ship. -
ignition_delay- seconds the torpedo coasts INERT before its drive lights. For that whole window it has no thrust, no guidance, no fuze and no colliders: it can neither be shot down nor touch the ship it is leaving. Size it againstspawner_speedso the motor catches once the torpedo is clear of the hull.0.0lights it on the first tick, which is the old launch-under-power behavior. -
nav_constant- the proportional-navigation constantN(typically 3-5; higher leads a moving target harder). -
linear_damping- drag on the torpedo body (gives a real terminal velocity so the flight path follows guidance). -
blast_radius,blast_damage- damage radius and peak centre pressure.blast_radiusno longer decides WHERE a torpedo goes off against a locked body (see the fuze); it is the reach of the pressure and the band the weave fades over. The visible sphere is the damage radius; pressure falls linearly to zero at its edge, measured from each collider's world centre. Ship sections shield sections behind them along a centre ray. A surviving section stops pressure; a destroyed section transmits 65 percent. Existing holes transmit freely. This transmission is one global Explosive rule, not an authored bay field. Structural sections can shield cladding and fixtures behind them, but those non-structural targets do not consume penetration themselves. Health is charged PER COLLIDER, but a blast cuts one crater PER BODY: a warhead over forty sections of one hull is one crater, not forty. -
blast_effect,launch_effect(both optional) - custom particle effects; omit for the built-in bursts. -
projectile_health(optional, default10.0) - hit points on each of the torpedo's two collider sections; either reaching zero shoots it down (silently, no blast). The default sits above the hardest single round a stock PDC can land (4.0 authored x the 2.0 kinetic speed ceiling), so an intercept costs a short burst rather than one lucky tap; author far more for armored ordnance point defense has to chew through across the closing window. -
torpedo_type(optional, defaults to the Serpent) - what the bay loads, as opposed to the tube it loads into. A type is DATA, not an enum: base authors its three (the straight-running Lance, the weaving Serpent, and the crimson siege Breaker - 70 u/s with a shallow 0.22 rad weave - that only the hiddenheavy_torpedo_sectionbay loads), and a mod authors its own by writing the same five fields:name- the ordnance's player-facing name ("Lance","Serpent","Breaker"). It names the launched projectile, so a log line or a probe snapshot says WHICH torpedo is in the air.tint- the warhead's colour in flight, asSrgba((red: .., green: .., blue: .., alpha: 1.0)). Two types a player is meant to tell apart want different colours: it is the only difference visible before the flight paths have diverged.max_speed(u/s;35.0on the Lance,32.0on the Serpent) - cruise speed cap. The thruster tapers off as the torpedo approaches it, so it decides time to target and, withprojectile_lifetime, how far the ordnance can reach. This is where an evasive type pays for its weave, and it has to be authored: see the note below.weave_angle(rad;0.44, ~25 degrees, on the Serpent) - the terminal weave: how far off the guidance solution an armed torpedo corkscrews. It PERTURBS the solution rather than replacing it and fades linearly to nothing between three blast radii and HALF a blast radius of the target, so a weaving torpedo still arrives on the aim point. The band is measured offblast_radius, never off the fuze: the terminal sprint has to start out at point-defense range, not a unit from the hull.0.0flies the bare intercept, which is what the Lance authors.weave_rate(rad/s;1.4on both shipped types) - how fast the weave spins about the guidance command. The lateral acceleration a defender's lead solution fails to predict scales withmax_speed * sin(weave_angle) * weave_rate, while the helix radius (max_speed * sin(weave_angle) / weave_rate) shrinks with it - keep that radius comfortably inside the terminal band,blast_radius * 0.5, or the torpedo is still swinging when the weave is meant to be gone. Unread at zero amplitude.
The ANGLE is the exchange: measured against one stock PDC across the shipped 150 u point-defense envelope, a Serpent costs ~370 rounds to stop and is only killed ~40 u out, where a Lance costs ~120 and dies ~115 u out. The RATE is the picture, not the price - the intercept cost barely moves with it, while the visible swing runs 24 u at 0.7 rad/s down to 6 u at 2.2.
A weave does NOT pay for itself, which is why the shipped Serpent authors a lower
max_speed. A corkscrew is a longer path, but only by ~1.7% as the real body flies it, and thrust is capped on the ALONG-NOSE speed - so a torpedo holding its nose off its own velocity never reaches the taper band, keeps its engine lit, and settles FASTER than a straight one. Author a weave without dropping the cap and you get evasion for free. Do not try to fix it by capping total speed instead: a total-speed cap leaves the torpedo ballistic at cruise and unable to steer at all. -
ammo_capacity(optional) - magazine size in torpedoes;Nonefor unlimited. -
reload(optional) - idle batch reload for the bay (needsammo_capacity), with the sameSome((delay, amount))shape as a turret. The shipped bay restores one torpedo after ten seconds without a launch. Another launch resets that timer. Ammunition is a rate limit, not a permanent budget, but a bay must win through its six-round salvo rather than by outwaiting one PDC.
The fuze is not a bay field
Where a torpedo goes off is engine behaviour, not an authored number. An armed torpedo that locked an ENTITY fuzes three world units from the nearest point of any collider linked to that body, or at the distance the torpedo covers in one frame, whichever is larger. The margin clears the torpedo's own body and the next physics step while keeping the pressure and crater on the target. Two other cases:
- a torpedo holding a target POSITION but no entity - a scripted launch, or one
whose target died in flight - fuzes at
blast_radius * 0.5, which is the only sense "arrived" has at a bare point; - a torpedo launched with NO lock never fuzes at all. It flies its
projectile_lifetime, deals a contact ding and is deleted, and the bay still spends the round. That is whatForceTorpedoLaunchproduces on a battery with no controller.
A section in a mod
The example mod (assets/mods/example/example.content.ron) overlays the base
reinforced_hull_section - same id, so it REPLACES the base part everywhere
(editor palette, ships) with more health and a renamed label:
Section((
base: (
id: "reinforced_hull_section",
name: "Reinforced Hull Section (Example Mod)",
description: "Base hull, up-armored by the example mod to show section overlay by id.",
health: 400.0,
),
kind: Hull((
render_mesh: Some("dep://base/gltf/hull-01.glb#Scene0"),
)),
)),
Reuse a base id to REBALANCE or re-skin that part; give a NEW id to ADD a part
alongside the base catalog. Either way, a ship references the section by id via
source: Prototype("<id>") in its sections list. To ship the section, package
the file as a mod - Publish a mod is the release flow.
Base ships a whole catalog of semantic ship-part prototypes. The mainline
corvette, gunship, and civilian hulls use cargoa_*, cargob_*, and
racer_* parts. A mod
does not have to inline a big ship or
carry any mesh paths: build one as a compact list of
(id, position, rotation, source: Prototype("<base-part-id>")) entries, and each
prototype resolves the base's meshes and sounds for you. Vary a ship by grade
with a per-spawn SetHealth modification on the parts you want
weaker - a scavenger flies the same pdc_kinetic_turret_section at 60 mount
health - rather than re-authoring the parts. SectionSource is Inline
(the full config, for a one-off part) or Prototype (a catalog reference, the
compact reusable form). Every prototype id base ships - with its kind, so you
can tell structure from a gun - is tabled in the
base content catalog.