← Dev Log
BallOrcs4 min read

Your orc turns up to the level — dressed, tinted, and told what it hasn't earned

Until this week the wardrobe and the game were strangers.

In plain words

Until this week the wardrobe and the game were strangers. You could build an orc — pick a skin, a hat, a nose, feet — and the game would save every choice faithfully and then put a plain grey ball in the level. That's fixed. The orc you built now shows up and plays: its parts ride the ball, the face stays the right way up while the body rolls underneath, and it tips a little into whichever way you're heading. Your colour choices finally reach the screen instead of sitting in a save file. And parts are no longer all free — hats, scarves and colour sets now unlock by relighting Founts and collecting orbs, so the barber's chair has doors on it that you open by playing.

One correction worth stating plainly: a BallOrc is the ball. Not a little orc riding one. We'd been writing it the other way round in the code, and it's now written the right way round.

Under the hood

Five threads landed, and they only work as a set — data, registry, renderer, gates and format all had to move together.

  • The registration gap is closed (6888f71, #12). 25 component definitions across 7 slots shipped as art that no running code could see. scripts/component_db.gd is now an autoload extending the ballorcs-core registry, loading every res://assets/<slot>/*.tres pack in alphabetical order (so the editor grid inherits a deterministic order), declaring per-slot fallbacks per FORMAT.md rule 2, and failing loudly at boot if a declared fallback didn't register. Unknown ids render the fallback and never mutate the character data.
  • The orc in the level (290cd6e, c753ed1, #14/#27). character_visual.gd follows the target body's global position per physics tick and never its rotation — that's the whole trick to components riding a rolling sphere. character_store.gd handles user://characters/<id>.borc, uuid minting, and a selection rule that returns null rather than inventing a default orc. L1 and L2 each gained one anchor node targeting ../PlayerBall.
  • The rider framing was wrong (3df1141, 34f9fc5). The skin component is the sphere torso and face; limbs and accessories float slightly apart. setup() now hides PlayerBall's placeholder grey sphere when a skinned character dresses, and restores it for skinless ones — no double-rendered ball. Then a deliberate game-feel call: the body leans into travel up to lean_max_degrees (10) at lean_full_speed (8 m/s), exponentially smoothed, on a tilt axis of UP × travel-direction — horizontal by construction, so yaw is impossible and the face keeps reading.
  • Colours reach the screen (2fa1e30, 8b911c9, #17). The core-module builder tints; ours only instanced, so colours round-tripped through every save and were then ignored. Populating colour_regions had to follow the meshes, not a checklist: 10 of 24 GLBs have one material and are now tinted; the other 14 carry several (eyes are Iris+Pupil+Sclera, hat_wick is Wax+Flame) and declaring a single region would flatten them — eyes become a blob. Those stay empty until the Blender pipeline emits per-mesh metadata. The tests derive that rule from the mesh, so they keep holding when the art changes.
  • Unlocks, system then data (17ab21b, 89ab292, 6ef1241, 1363067, #42). unlocks.gd awards fount families by level completion and colour/style sets by orb totals — summing best-per-level, so respawn farming can't creep the counter, and never revoking. Then the data half, mapped from GAME_DESIGN.md's own slot table rather than invented: hat.wick → fire behind fount.fire, hat.propeller → flight behind fount.sky, accessory.scarf → speed ×1.10. The subtle one: the hat slot's fallback had to move, wick → horns. Gating the fallback would leave pre-L2 players resolving unknown ids to a locked part — silently, and only for players who hadn't earned the flag. fallbacks_are_never_gated now pins that invariant so the next person to add a gate meets a red suite instead of a bug report.
  • The format grew a v2, migration-free (2fa1e30, 518f6bb, #11/#16). Nose and feet are purely additive: a v1 document needs no transform and a reader must not invent defaults for the new slots. That policy was prose and nothing executable — the existing round-trip test passes just as well if a reader defaults a slot and echoes it back. Four new cases, two of them forcing functions: corpus_covers_every_schema_version fails the next schema bump that ships without a fixture, and future_fixture_stays_ahead catches the day the "document from the future" quietly becomes a current document. 30 checks / 8 tests → 44 / 12, inside the job CI already runs.

Also this week, unglamorously: the automated visual check finally ran end to end — 117 of 120 screenshot anchors pixel-identical across two runs of the same build. The three that moved are all spawn framings, and we know why: the lean above is exponentially smoothed while the camera probe captures after a fixed frame count, so it photographs a tilt still settling. Recorded honestly rather than papered over.

#characters#components#unlocks#colours#format