← Dev Log
BallOrcs5 min read

Which bit of an eyeball is the eye, and a fix that took three goes

Your orc lets you pick colours for almost everything — but not its eyes.

In plain words

Your orc lets you pick colours for almost everything — but not its eyes. Eyes were the one part of the wardrobe with nothing you could change, and the reason is that an eye is not one shape. It is three: the white, the coloured ring, and the dark pupil. The game had no way to tell which of those three you meant, and colouring all of them turns an eye into a featureless dot. So this week was spent labelling each piece inside the art itself, and then chasing those labels through every step between the art tool and the game — because they were being quietly thrown away in transit. Tutorial text can also be made bigger now.

One of the eye components, straight from the generator: the white, the coloured iris and the pupil are three separate pieces of geometry. Only one of them should ever take your chosen colour.

Under the hood

The whole week is one chain, and every link had to land in order, because any link on its own produces a flattened eye rather than a tintable one.

  • Label the meshes (8dfe720, 0792cc1, 491934b, refs #102/#106). generate_eyes.py now marks each mesh with a colour_region: iris → "eyes", sclera and pupil → "". Marking the last two is not redundant — once the defs declare ["eyes"], appearance.gd's implicit fallback hands that single region to every mesh without metadata, so the sclera and pupil would tint too. An empty region is already the "do not tint" signal (if region.is_empty(): continue), so opting out needed no new convention. The eye also gets its own region rather than reusing primary or secondary, so eye colour is chosen independently of body and accent.
  • Blender was dropping the labels entirely. export_extras defaults to False, so custom properties never reach the .glb — the properties would have been set, visible in Blender, and simply absent from the shipped asset, importing cleanly and never tinting. Now enabled in generate_eyes.py, generate_mouth.py and the shared export helper, so the next per-mesh region does not rediscover it.
  • One variant of four was missed. build_ember does not call build_eye; it inlines its own copy of the same construction, so eyes_ember stayed unmarked. Fixed by moving the three assignments into tag_eye_regions() and calling it from both sites — a function, not a second copy, because the duplication is the bug.
  • The game had to learn where Blender puts them (core pin 477299d → 52f7ba4). Godot's importer does not give each custom property its own key; it puts them all under a single extras dictionary. _region_for now reads that dictionary as well as direct metadata — without it the entire art-side plan produces GLBs that import cleanly and never tint. The same bump makes the editor say why its colour section is empty, instead of reading as broken.
  • The safety net was about to reject the correct fix (9be8bdf). component_db's colour_regions_match_materials demands that multi-material parts with no per-mesh metadata declare zero colour regions. Its helper only checked direct metadata, not the glTF extras form — so the moment the regenerated art arrived, it would have failed the right change while insisting the eye would flatten, which by then would not be true. Verified by removing each half and watching it fail, not by watching it pass.
  • Half-done tagging now fails too (4b8ed83). The guard asked "does any mesh carry metadata", so a part with one mesh tagged and two untagged slipped straight through — and flattens exactly like the untagged case, silently, while looking correctly authored in the diff. _meshes_without_region_meta walks meshes the same way _region_for does and names the ones that resolve nothing.
  • The shared character module's tests now run in CI (db41005, closes #104). verify_editor.gd covers the editor's focus contract, preview tree, colour application and unlock gating — and no job ran it. It passed whenever someone ran it by hand and gated nothing in between. A thin wrapper (rather than a case in the runner) so the module keeps owning its own tests. The suite count is now 8.
  • The screenshot flake, third attempt (c1ee782, 4c3cd8c, #100). Two earlier fixes moved the numbers not at all, because two things were wrong at once. First, the wait watched the PlayerBall — but the lean is applied by CharacterVisual to itself, and that node is a sibling of the ball, so the ball can be perfectly still while the visual is mid-slerp. Second, even watching both, the test compared each frame to the previous one against SETTLE_EPSILON = 0.0005 — and the measured drift was 0.314 in Y spread over ~600 frames, almost exactly 0.0005 a frame. Every individual frame looks stationary while the subject descends continuously. The loop now measures drift from a reference pose captured when the still-streak began, so cumulative movement keeps failing the test instead of passing it one frame at a time. Still unproven: playtest does not run on merge-request pipelines, so the first soak on main is the test.
  • Tutorial prompts scale up (63592ae, part of #78). ui.prompt_text_scale (1.0–2.0) joins the settings surface with a new Accessibility section. It scales up only — the floor is the authored size, because letting a player shrink the game's teaching text is a way to get stuck, not a preference. Two non-obvious layout facts fell out: grow_vertical must be GROW_DIRECTION_BEGIN or a wrapped three-line prompt grows down through the bottom margin and off-screen, and the settings autoload is registered after the prompt manager, so applying the scale has to be deferred or a player who chose 2× silently gets 1× back on every restart.
  • Two dependency pins stopped crying wolf (fdd4dbb). The scons and emscripten entries were reported as BEHIND every week, but neither pin selects anything — both tools arrive inside the build container, and the recorded values are documentation of what that image ships. Editing them to match upstream would not change a byte of any build. They now report drift as info only, naming what actually decides the value.

Honest status: the eyes are not tintable in this build yet. The last link — the .tres defs declaring ["eyes"] — is deliberately still on a branch, because declaring it before the art carried metadata is exactly the failure the guards above exist to catch.

#characters#art#pipeline#testing#accessibility