| name | ressort-vehicle-asset |
| description | Build a rideable vehicle asset (helicopter, car, bike, plane, tank) for the machin-ressort engine — silhouette, rig, palette, and a baked rotation ladder. Use when adding any non-character sprite that has to tilt, lean, or turn, or when a `.sil`/`.rig` needs authoring by hand rather than generating. |
Authoring a vehicle for ressort
The generator (body_new) makes people. Vehicles are hand-authored, and every
tool in this repo was written for characters first — so a vehicle trips the
same five assumptions every time. This is the order that works and the traps in
the order you hit them.
The pipeline
ressort sprite import ref.png --native 104x44 --colors 24 \
--key e0d9c9 --key-tol 34 --compare cmp.png
ressort sprite shade assets/thing.sil --out assets/thing.spr --pal desert
ressort sprite rig assets/thing.spr --rig assets/thing.rig
ressort sprite tilt assets/thing.spr --rig assets/thing.rig \
--steps 9 --range 45 --sheet out.png
An import gets you the palette and the proportions. It is not the asset:
anything that must move separately is baked into the drawing at one angular
position. Import as an underlay, author the .sil against it.
Rule 1 — the pivot is where force is applied
This is the decision everything else follows from. A vehicle is not a
skeleton; almost nothing moves relative to anything else. The question is what
the whole body rotates about.
| Vehicle | Pivot | Why |
|---|
| Helicopter | rotor head | thrust acts there |
| Car / bike | contact patch between the wheels | weight transfer pitches about the tyres |
| Bike (lean) | tyre contact line | it leans about where it touches |
| Plane | ~1/4 chord of the wing | lift acts there |
| Tank turret | turret ring | it literally turns there |
Put it anywhere else — the sprite centre is the tempting mistake — and dropping
the nose swings the tail the wrong way. It reads as a boat turning instead of
an aircraft tipping.
Set the .sil anchor to that same point. Then world position is the pivot
and no offsets are needed anywhere else.
Attachment points rotate about that same pivot. A cargo hook, a tow hitch,
a hardpoint, an exhaust, a muzzle — each is a body-local offset, and each must
be rotated by the body's angle before it is used:
hook_x = x - D * sin(theta) // a point BELOW the pivot swings aft
hook_y = y + D * cos(theta) // as the nose drops
Leave them in world space and they look welded on. Mark them in the drawing
with their own zone character so the game can find them instead of carrying a
magic number — HOOK_D() = 33.0 in this repo is a measurement off a picture,
and it will be wrong the first time the hull is reshaped.
Rule 2 — anything that moves alone needs an EXCLUSIVE band
The rig owns axis-aligned rectangles, drawn back to front by z. A part
whose rectangle the hull also covers is redrawn by the hull on top of it. It
will rotate underneath and never be seen.
So the layout is dictated by what animates, not by the reference art:
part hull x=10,120 y=6,52 pivot=66,8 z=2
part rotor x=0,120 y=0,6 pivot=66,5 z=3 parent=hull # own ROW band
part tail_rotor x=0,10 y=6,32 pivot=6,18 z=1 parent=hull # own COLUMN band
For a car: wheels need their own column bands, or the body draws over them and
they never spin. Plan the drawing around this before you draw it.
Rule 3 — bake the rotation ladder, do not rotate per frame
rig_pose_g has a virtual root that rotates an entire sprite about an
arbitrary point. Bake N poses once at load and index them:
frames := tilt_bake(sp, rg, 17, 48.0, 30) // steps, degrees, pad
fr := frames[tilt_index(theta, 17, 48.0)]
Seventeen rasterisations at load instead of sixty a second, deterministic, and
the physics just indexes it. tilt_index rounds — truncating puts level
flight half a step nose-down forever.
Bake a range slightly wider than the physical limit (48° for a ±45° limit) so
the extremes are not the clamp.
Seen from above, it is the same idea without the rig (and the world it
drives through is ressort-top-down-world). A top-down vehicle
has no joints to pose, so the bake rotates the finished sprite itself, and it
needs the whole circle rather than an arc:
frames := spr_bake(veh_draw(spec)) // engine/65_bake.src, 64 headings
fr := frames[kind * SPR_HEADINGS() + spr_head(theta)]
Sixty-four is 5.6° apart, which is finer than a 26 px sprite can express — more
would rasterise identically and fewer would visibly step as you round a corner.
spr_head rounds, for the same reason tilt_index does. Ten vehicles ×
64 is 640 sprites, about a second at load, and a frame that never rotates
anything.
Inverse-sample the destination (walk the output and ask where each pixel came
from); forward-mapping leaves holes, and the holes are worst at exactly the
angles a vehicle spends most of its time at. Assert it: count the non-transparent
pixels at 0° and 45° and require the second to be within ±30% of the first.
Rule 4 — one bake, many colours, and the slot that ruins it
Ten colours do not mean ten bakes. Rasterise the paintwork in a sentinel
palette slot and hand the draw call a palette with that slot replaced
(pal_swap, engine/65_bake.src); a hundred vehicles in ten colours cost one
bake.
The sentinel must sit OUTSIDE the range of colours it can be swapped for.
Ours was slot 10, which was also the first body colour — so every decal painted
in slot 10 was repainted along with the bodywork, and the ambulance's red cross
and the police light bar came out in whatever colour that vehicle happened to
be. Put the sentinel past the end of the palette's body range and assert it:
T_PAINT() >= PAL_BODY_LO() + PAL_BODY_N() // and still inside the palette
Rule 5 — ten vehicles means ten SILHOUETTES, plus one mark each
Generate the shape and the handling from one spec — length, width, where the
cabin sits, how the nose tapers, power, top speed, turn rate, mass — so a thing
that looks heavy drives heavy. That is the only reason a player would ever
prefer one vehicle over another.
But a silhouette at 26 px only says big or small. What names a vehicle is a
single identifying mark: an open ribbed bed, a full-width box body, a row of
windows down the flanks, a roof sign, a red-and-blue light bar, a red cross,
racing stripes. One mark each, and ten vehicles stop being one vehicle in ten
colours. Assert that no two share (len, wid, deco).
The paint has to win. The first pass gave every car a dark roof and dark
wheels and the result was a dark blob at every heading, indistinguishable forty
at a time on a street. Hull in body colour end to end, cabin as glass rather
than shadow, and the only black a one-pixel silhouette that lifts it off the
tarmac.
The five character assumptions you will trip
.sil has no comment syntax except ;. Everything that is not a
directive is art. Three lines of prose become ~3000 stray pixels, silently.
It cannot be # — that is the commonest art character in the format.
shade --out is the only way to bake a hand-authored .sil. The other
path is sprite check, which gates on the project's character canvas
(24x40) and will reject any vehicle.
sprite check is a character checker. It will complain about canvas
size and "move the figure so its feet land on the anchor row". Both are
correct about a person and wrong about a vehicle. Use sprite rig for the
check that matters (coverage) and ignore the rest.
- Materials name palette SLOTS, not colours. Slot 1 is skin, so
sand
rendered a helicopter the colour of a person until --pal desert repainted
all twenty. Add a palette (pal_named) rather than fighting the ramps.
- The frame sheet stands sprites on their anchor. Correct for feet; a
rotor-head anchor hangs the vehicle off the bottom of the page. Already
handled for top-half anchors, but know why it looks odd.
Shading a big flat body
Depth shading is for limbs. Every interior pixel of a fuselage is equally deep,
so a three-step ramp paints the whole thing its darkest step and any markings
have nowhere left to go.
- Vehicle materials use two-step ramps (
sand = "12"), keeping a lit rim.
- Let the drawing carry panels, hatches and lines. That is where a
vehicle's detail lives.
- Paint markings carry no outline (
camo = "3", outline 0) — blotches
that ring read as objects sitting on the hull, not paint on it.
- Do not dither camouflage. It becomes speckle that swallows the panel
lines. Solid patches with a ragged edge read as paint.
Drawing it
52 rows of 120 characters is not something to type. Generate the .sil from
shape functions (see tools/mkhelo.py) and iterate on the render. Define the
body by its top and bottom edge per column, not as a stack of rectangles —
the first helicopter ramped every edge into every other one and read as a fish.
A transport is a box with a cockpit stuck on the front.
The .sil is the committed source of truth afterwards; the generator script is
scaffolding you keep for reshaping.
Scale
Pick pixels-per-metre for the whole game and hold every asset to it. The helo
is 115 px for 11.5 m (10 px/m). A generated humanoid is 40 px for 1.8 m, which
is 22 px/m — the two are on scales differing by 2.2×, an unresolved gap in
this repo. Decide this before authoring, not after.
Checklist