← Dev Log
BallOrcs5 min read

Make your orc at the door, and change your mind at the barber's

Last week the orc you built finally turned up in the level. This week there is a way to build one.

In plain words

Last week the orc you built finally turned up in the level. This week there is a way to build one. Start Level 1 on a fresh save and the game stops and asks: pick a skin, a hat, a nose, feet. Before, the editor existed but nothing in the game ever opened it, so a new player got the grey placeholder ball and no way out of it. And because first choices are rarely final, rolling into a barber's chair reopens the same editor on the orc you already have, and the change lands on the ball in front of you straight away. The robot playtesters skip the whole thing — a modal that waits for a human would stall them until the heat death of the soak.

Under the hood

Two features and a housekeeping pass on checks that were not checking.

  • Creation at the door (9069fd7, #27). Shaped like story_beats.gd: an autoload watching current_scene, keyed by level id, so no level scene is edited. That is not tidiness — level1.tscn's root is GameRoot, a C++ GDExtension type that cannot instantiate in a headless sandbox, so a scene-embedded node would have been untestable outside a full host build. It opens only on a creation level and only when the profile has no character; it seeds each slot from that slot's fallback, the one thing guaranteed ungated, so a brand-new profile can wear every slot; it passes Unlocks.gate_for(save) so earned parts stay earned. Cancel writes nothing — character_store routes a missing character back to creation rather than papering over it with a silent default. Two bugs the new tests caught while being written: const EditorScript shadows a native Godot class and silently took the whole autoload down, and _close() called get_tree() on a detached node, which logs an error before returning null.
  • The barber's chair (0193b8f, #43). Both entry points instance the same editor through CharacterCreation, so the tutorial chair and the shop cannot drift apart. Every judgement — playtest safety, refusing to stack a second editor, loading the existing character, saving over the right id — lives in reopen_for_edit(scene), which returns whether it opened so a caller can tell "opened" from "declined" without reaching into private state. scripts/barber_chair.gd is a deliberately logic-free Area3D: it references PlayerBall, which comes from the C++ extension, so nothing in it can run headlessly. A real bug fixed in passing: _on_done minted a new character id every time, so each visit would have written another .borc and orphaned the last one — invisible except as a growing user://characters/. Suites now stand at 328 checks / 63 tests, all eight green.
  • The suites that gated nothing (8a300d3, #101). CI ran tests/character_format and nothing else ran a suite at all, so 239 checks across five suites existed and stopped no regression from reaching main green. The case that prompted it: #17's guard against giving a multi-material component a colour region — which would flatten eyes to one colour, worse than the bug it fixes and invisible in a diff — lives in tests/component_db, merged and never once run by CI. Suites are now discovered, not listed, each in its own engine invocation so one aborting cannot take the rest with it, and an absent project/tests/ dies rather than matching no glob and passing silently.
  • A correction to last week's last paragraph. We reported 117 of 120 screenshot anchors pixel-identical and blamed the three stragglers on the lean smoothing still settling when the shot fires. The real cause is one line (2c0585c, #99 + #100): run_one(launcher, "camera", [], ...) passed the camera probe no engine flags at all. It is the only non-headless instance in the swarm, so it alone missed what --headless implies — --audio-driver Dummy, which is why eight headless bots log zero engine errors and the windowed probe logs one in a container with no sound device, and --fixed-fps, without which the probe runs in real time and the ball is somewhere slightly different when each shot fires. Spawn is the only anchor that frames the character; the rest are level geometry, which is why 117 were already stable. Not yet verified that the errors vanish — that needs a soak. Two consecutive runs on an unchanged tree should report changed: 0.
  • CI stopped building the same game for every typo (de6c18e, bcfa93f). Measured 2026-07-26: four consecutive commits touched only docs/cowork-handoff.md and each triggered four platform builds plus the soak — roughly 48 minutes of a concurrent = 1 runner per markdown edit, and 99% of all measured job time went there. rules:changes now gates build, package and playtest. In the same breath the soak went automatic on main instead of when: manual: the appearance forwarder went a week unexercised because nobody pressed the button, and when it finally ran all nine runs passed — so the gate bought nothing and hid the one check that could have said so. The follow-up commit is the joke writing itself: tools/playtest/** was missing from the path list, so a change to the soak runner would not have triggered the soak. The soak's own source, unsoaked.
  • Four yellow runs nobody read (793d493). dependency-freshness had failed 4 of 4 runs in the last 20 pipelines. Not stale dependencies — a comparison that can never be equal: godot-cpp is pinned to 10.0-master-20260629 because the <cstdint> fix is master-only, while every upstream release tag looks like godot-4.5-stable. The fix is to consult what versions.lock already declares one line below (GODOT_CPP_GIT_TAG="", "no tag exists for this pin"), so comparison resumes on its own the day someone re-pins. A second defect surfaced while testing it, same theme as everything above: (could not query) never set behind, so a total network failure printed "All queryable dependencies are current" and exited 0 — six failed queries reported as a clean bill of health.
  • Noted, not fixed (8e68197). A review of the appearance and unlock path, checked against the running registry rather than by reading, found that an outfit of entirely legal parts — bubbling skin, wide eyes, fangs, wick hat, tail-flame, snout, boots — yields zero colour rows, and the colour section silently disappears. The #27 seed yields two, so first-run is fine; this bites players who customise into multi-material parts. It is the visible cost of #17's correct conservatism, and the real fix is per-mesh colour_region metadata out of Blender.
#characters#creation#barber#tests#ci