← Dev Log
BallOrcs8 min read

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-wrapping HBoxContainer, 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_apply adds is not an assertion but a fixture: the old character_creation suite proved _dress reaches "a CharacterVisual" 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 real barber_shop.tscn, the real CharacterVisual and 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, alongside ballorcs_core_editor 92 across 17 and character_creation 45. 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 .glb is deliberately a single foot at the local origin — project/assets/feet/license.txt says so — with left/right placement and the walk bob applied by the player scene's own FootL/FootR nodes. 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 !11 gives ComponentDef a mounts array; 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 .tres files 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_placement is 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, a hay_boulder.gd prop, a level-select entry and a greybox_catalog suite — and then gains the thing that makes it a level rather than a space: restoration. restoration.gd and restoration_hud.gd make putting the place back together the way you win, with goal_flag.gd and orb.gd wired into it, a hook in playtest_driver.gd so the robots exercise it too, and a 191-line test suite. First slice only, and docs/LEVEL_PLAN.md carries the rest.

  • The lock file had described a core the game did not ship, for a month (c548d1f, #136). Two things record which ballorcs-core this 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 — reads BALLORCS_CORE_GIT_COMMIT out of versions.lock. Nothing compared them, and they had drifted: the lock said 52f7ba40 (2026-08-12) while the gitlink pointed at cec7f81b (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 touches versions.lock, and nobody compares a 40-character hash to a quoted string by eye. ci/verify-core-pin.sh reads the gitlink with git 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. Not allow_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.md is 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 writes dist/<platform>/build-manifest.json with a sha256 per file, and the panel shows how to check a download against the pipeline that produced it. The sha256sum pipeline 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.uid is 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 into versions.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 no done flag, so backlog.json read 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 and json.dumps would 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, because done: true is 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 shows done as the only field that differs anywhere. seed-backlog.sh gains the --dry-run its own verify step had always asked for — tested with a stubbed urlopen that 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 the gitlab service and left the runner up. Backup first, including the parts gitlab-backup does not take — gitlab-secrets.json and gitlab.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/hosts entry and the cache's --add-host are 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).

#playtest#characters#levels#testing#ci#infrastructure