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.

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.pynow marks each mesh with acolour_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_extrasdefaults 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 ingenerate_eyes.py,generate_mouth.pyand the shared export helper, so the next per-mesh region does not rediscover it. - One variant of four was missed.
build_emberdoes not callbuild_eye; it inlines its own copy of the same construction, soeyes_emberstayed unmarked. Fixed by moving the three assignments intotag_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 singleextrasdictionary._region_fornow 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'scolour_regions_match_materialsdemands that multi-material parts with no per-mesh metadata declare zero colour regions. Its helper only checked direct metadata, not the glTFextrasform — 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_metawalks meshes the same way_region_fordoes and names the ones that resolve nothing. - The shared character module's tests now run in CI (
db41005, closes #104).verify_editor.gdcovers 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 thePlayerBall— but the lean is applied byCharacterVisualto 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 againstSETTLE_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:playtestdoes 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_verticalmust beGROW_DIRECTION_BEGINor 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.