A human played it at last, and the feet were inside the ball
Somebody who is not a computer finally sat down and played BallOrcs, and the report was blunt.
In plain words
Somebody who is not a computer finally sat down and played BallOrcs, and the report was blunt. The barber shop did not work: your changes never reached your orc, and there was no button to confirm them. Feet, when you picked a pair, were invisible. Both turned out to be the same kind of mistake — something quietly grown past the space it was given. The barber's palette got wide enough to shove the Done button off the edge of the screen, and feet were being drawn as one foot at the very centre of the ball, hidden inside it. Both are fixed. A new grassy level called Longfield has also appeared, with a new reason to finish it: put the place back together.
Under the hood
The Done button was off the screen, and that is the whole bug (
5ec9a81, #56 item 8). The playthrough report was "barber-shop changes do not apply to the player character, and there is no Apply button" — both halves, one cause, and it was never the save path. Each colour region's swatches sat in a non-wrappingHBoxContainer, so the editor's minimum width was the whole palette laid end to end; a container's minimum is a floor the engine overflows rather than compresses. Measured against this game's 1280×720 canvas:swatches min width Done 12 (fresh profile) 1074 on screen 16 (+ Ember, 10 orbs) 1250 30 px to spare 20 (+ Tide, 25 orbs) 1426 at x=1282 No Done button, no done signal, no save. #43's acceptance was honest when written — the palette went from 12 swatches to 20 afterwards, when gated colour sets landed. Fixed in core
05925ec: swatch rows wrap and the colour section scrolls, so the editor's minimum is a constant 830×500 whatever the player has unlocked.The test that passed all the way through it was testing a stand-in. What
project/tests/barber_applyadds is not an assertion but a fixture: the oldcharacter_creationsuite proved_dressreaches "aCharacterVisual" using a duck-typed stub, and it stayed green for the entire period the feature was broken for a human. The new suite uses the realbarber_shop.tscn, the realCharacterVisualand the real editor, and asserts the three things in the order the player meets them: the editor fits the canvas with every colour set unlocked, the edit reaches disk under the same character id, and the body in the world changes to match. 17/17, alongsideballorcs_core_editor92 across 17 andcharacter_creation45. The barber's screen is now titled "Change your look" rather than "Your BallOrc" (#56 item 9) — the editor takes the title as an argument, so the shared module never learns what a barber is.A convention with two consumers, and the shared builder was not one of them (
9b201bf, #56 items 7 & 40). Reported on Windows: the feet do not appear when selected. On web: they appear, but small and at the centre of the character. Same geometry, two lightings. The feet.glbis deliberately a single foot at the local origin —project/assets/feet/license.txtsays so — with left/right placement and the walk bob applied by the player scene's ownFootL/FootRnodes. The shared appearance builder had never been told. Measured in the VM against a body of radius 0.50, the stubby foot's aabb is 0.11 × 0.06 × 0.12, a reach of 0.10 — entirely inside the ball, and under an opaque skin, invisible. The asset was never wrong and neither was the def; a convention simply had no home in the data.mounts, and an empty array means what every part already did. Core!11givesComponentDefamountsarray; empty — every part shipped so far — means one instance at the origin, unchanged. The three feet defs now declare two mounts each, at the base (y −0.5), forward (z −0.15), straddling the centre line (x ±0.18), at 2.5× the authored scale so they read at gameplay camera distance. After: the pair spans x −0.31…0.31, y −0.50…−0.34, reaching 0.66 — in the same range as the eyes (0.75) and the hat (0.61). Those numbers are data in three.tresfiles and are meant to be felt rather than argued: if they read wrong on the re-test build they are three lines to change, with no code behind them.project/tests/appearance_placementis the check that would have caught it the day feet landed — every slot's rendered mesh must reach at least 0.40 from the orc's centre, so a part may sit proud of the surface or be partly sunk into it but may not hide inside the body. Checked both ways: 24 checks pass, and with stubby's mounts deleted it fails naming the 0.10.Longfield, and a win condition that is not a finish line (
b9e3f0a,cfcf1bd, #122, #91). Zone I level 1 lands as a greybox —project/levels/longfield.tscn, ahay_boulder.gdprop, a level-select entry and agreybox_catalogsuite — and then gains the thing that makes it a level rather than a space: restoration.restoration.gdandrestoration_hud.gdmake putting the place back together the way you win, withgoal_flag.gdandorb.gdwired into it, a hook inplaytest_driver.gdso the robots exercise it too, and a 191-line test suite. First slice only, anddocs/LEVEL_PLAN.mdcarries the rest.The lock file had described a core the game did not ship, for a month (
c548d1f, #136). Two things record whichballorcs-corethis game uses: the build reads the submodule gitlink, and everything a human reads — the freshness report, the docs, a release note, the first line of a crash triage — readsBALLORCS_CORE_GIT_COMMITout ofversions.lock. Nothing compared them, and they had drifted: the lock said52f7ba40(2026-08-12) while the gitlink pointed atcec7f81b(2026-08-23) — four commits and eleven days further on. Found by accident while fixing the barber. It drifts silently by construction: bumping a submodule is a one-line diff that never touchesversions.lock, and nobody compares a 40-character hash to a quoted string by eye.ci/verify-core-pin.shreads the gitlink withgit rev-parse HEAD:<path>— out of the commit, not the working tree, because the point is what the commit claims rather than where a developer's checkout happens to sit. Notallow_failure, deliberately: "something newer exists upstream" is a fact about the world and yellow suits it, but "the repository contradicts itself" is always actionable.Release notes written from the record, not from the prose (
4ffe3cc, #57).docs/RELEASE_NOTES_V1.mdis built from the milestone history as the server holds it — L1 29 issues closed, L2 12, L3 11, P1 23, V1 4 — so it says what actually shipped. P1 gets a section of its own because it closed two weeks early: a reader comparing the build against the milestone dates would otherwise conclude the polish pass was still outstanding. The blog's "play this build" box becomes a distribution panel: per-platform landing paths, the caveat that a wasm build needs a real server, and a verification step — every build already writesdist/<platform>/build-manifest.jsonwith a sha256 per file, and the panel shows how to check a download against the pipeline that produced it. Thesha256sumpipeline was run against a real manifest and checked to fail on a tampered one.Re-vendoring the crash SDK stops being a by-hand procedure (
461f3b0, #57). Ported from the sister project, where doing it by hand cost six distinct faults in one evening — one aiming 121 MB of binaries at the gitlab.com mirror, one deleting the vendored tree and pushing the deletion. Two deliberate differences here:sentry.gdextension.uidis generated by the Godot editor, committed in this repo and absent from every upstream archive, so a re-vendor that simply replaced the tree would drop it and the next editor run would mint a different uid, quietly re-pointing every reference to the addon — it is carried across from HEAD, and its absence there is a hard stop. And the tree digest is printed rather than written intoversions.lock, because this repo has no gate reading it yet: a pin nothing reads is how a lock file starts lying.The plan file said five issues were done; the server said 102 (
dcd4960). 67 entries whose issue was closed carried nodoneflag, sobacklog.jsonread as though nearly the whole slice were outstanding. The edit is surgical rather than a re-serialise — the file is hand-formatted one line per issue andjson.dumpswould have reformatted all 78 KB for a 67-field change, so"done": true,is inserted in place: +938 bytes, zero line-count change. Safety checked rather than assumed, becausedone: trueis what makes the seeder close an issue: every flip was gated on the server already reporting that issue closed, and a deep compare of the parsed file before and after showsdoneas the only field that differs anywhere.seed-backlog.shgains the--dry-runits own verify step had always asked for — tested with a stubbedurlopenthat raises on any non-GET: 0 writes attempted, 139 planned.A CVSS 10.0 patched the day it was found (
cd03f71). GitLab CE 19.1.6 → 19.1.8 for CVE-2026-85706, a path traversal in the repository commits API affecting "all versions from 18.7 before 19.1.8" and already being scanned for in the wild. Same minor, so no required stop and no background-migration wait; compose recreated only thegitlabservice and left the runner up. Backup first, including the partsgitlab-backupdoes not take —gitlab-secrets.jsonandgitlab.rb, without which a restored backup cannot decrypt its own CI variables. Verified after rather than assumed: the API reports 19.1.8, the registry answers 401 on/v2/, and all four runners reconnected on their own.A registry address that was true from exactly one machine (
5a40976,73ea9d4). The token realm pointed at box 1's loopback, which a remote puller follows to its own loopback. Naming box 1's LAN address instead fixed box 2 and broke every pull on box 1: WSL2 does not hairpin to the Windows LAN address it lives behind. The realm is now a name that resolves to the right place on each machine, and that line lands last, because the/etc/hostsentry and the cache's--add-hostare inert on their own and must both be in place first.Also: the playtest job is pinned to box 1, whose perf baseline it compares against (
0d0f47c, #135) — a baseline compared against a different machine's numbers is not a baseline; and the #132 GlitchTip grouping transform is now pinned by a test and made testable in the first place (f1fe0cc).