# Ressort Vehicle Asset

> 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.

- Skill: `javimosch/ressort-vehicle-asset` (Agent Skill)
- Install (CLI): `npx skillmds@latest add javimosch/ressort-vehicle-asset`
- Raw SKILL.md: https://api.skillmd.com/api/skills/javimosch/ressort-vehicle-asset/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: javimosch (https://skillmd.com/u/javimosch)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/javimosch/ressort-vehicle-asset

---


# 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

```sh
# 1. reference → proportions (optional, never commit the PNG)
ressort sprite import ref.png --native 104x44 --colors 24 \
    --key e0d9c9 --key-tol 34 --compare cmp.png

# 2. author the drawing, then bake it
ressort sprite shade assets/thing.sil --out assets/thing.spr --pal desert

# 3. the rig must cover the drawing — 100%, not 98%
ressort sprite rig assets/thing.spr --rig assets/thing.rig

# 4. bake the rotation ladder and LOOK at it
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

1. **`.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.
2. **`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.
3. **`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.
4. **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.
5. **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

- [ ] pivot = where force is applied, and `anchor` agrees
- [ ] attachment points (hook, hitch, muzzle) marked in the drawing, and
      rotated about the same pivot rather than pinned in world space
- [ ] every animated part has an exclusive band
- [ ] `sprite rig` reports 100% coverage
- [ ] rotation ladder baked and **looked at**, not just measured
- [ ] palette named for the game, not inherited from the character set
- [ ] one connected silhouette (flood-fill it; disconnected parts are usually a bug)
- [ ] reference PNG **not** committed — the transcription is the asset
- [ ] (top-down) full-circle bake, and pixel count preserved at 45°
- [ ] paint sentinel outside the body-colour range, and asserted
- [ ] no two vehicles share a silhouette; each has one identifying mark

