Move the 40-odd root-level scripts and scenes into a tree that groups by role,
per the approved roadmap (WS2/WS3): actors/{player,matador,bear}, systems/{combat,
fx,input,debug,render,crowd,config}, ui/, props/ (was tscn_s/). Rename the shell
scene.tscn -> levels/game_shell.tscn and every PascalCase scene to snake_case
(Player.tscn -> player.tscn, MainMenu.tscn -> main_menu.tscn, etc.) so the
case-insensitive web/macOS filesystems can't alias two files together.
Done as a scripted move: each file travels with its .uid; every quoted res:// path
across scenes, scripts, .tres, and project.godot's 9 autoloads is rewritten; the
.godot uid/class cache is rebuilt from scratch. Scenes still resolve by uid, so the
rewrite is belt-and-suspenders. Lint globs (run_tests.sh + both CI workflows) now
walk the new dirs; an orphan animation_tree.gd.uid and 7 pre-existing trailing-
whitespace lints (barrel/torch, never covered by the old root-only glob) are cleaned.
Full suite green on both sides. Asset consolidation (Assets/Animations/fonts ->
art/) is deferred — moving imported .glb/.png/.ttf is safer done in the editor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
Tests
Headless, deterministic tests that run in CI (.github/workflows/ci.yml) and locally via
bash run_tests.sh. Nothing here needs a GPU — even the UI checks assert computed geometry
instead of comparing pixels, so the whole suite runs in plain headless Godot.
Run
bash run_tests.sh # lint + every headless test (+ gameplay assertions)
bash run_tests.sh --no-gameplay # skip the slower full-scene gameplay pass (what CI runs)
# a single test:
godot --headless --script tests/<name>.gd
Set GODOT to point at a specific binary: GODOT=/path/to/godot bash run_tests.sh.
The six layers
Order is cheapest first. The middle layers are where "don't break interactions/UI by accident" is actually won — none of them need a screenshot.
| Layer | What it drives | Example here |
|---|---|---|
| Scene smoke | Instantiate a scene headless, assert no errors | level_switch_test.gd (level modules load, no geometry leaks) |
| Unit / logic | Pure functions, no scene tree | logic_test.gd; harness_selftest.gd (proves test_helpers.gd) |
| Signal contract | Emit a signal, assert args/count | to add: died / health_changed / all_defeated fire correctly |
| Interaction | Feed synthetic input, step frames, assert state | touch_controls_test.gd, matador_charge_test.gd |
| Scenario / replay | Scripted inputs + fixed seed over a whole fight | difficulty_sim.gd (a report), bear_boss_test.gd |
| UI layout | Resize the viewport, query Control rects |
ui_layout_test.gd (on-screen, text fits, tap size, scroll) |
Shared harness
test_helpers.gd (class_name TestHelpers) holds the primitives every test used to re-implement:
TestHelpers.make_rng(seed)— a seededRandomNumberGenerator. Determinism is the precondition for the interaction/scenario layers; seed the RNG here instead of the globalrandf()/randi()so a test replays identically. (Reworking gameplay code to accept an injected RNG is tracked in the refactor plan's Workstream 2.)TestHelpers.approx(a, b, eps)/vec_approx(a, b, eps)— float/vector comparisons with tolerance, because computed positions never compare exactly equal.- A pass/fail tally:
t.ok(cond, msg)andt.report(), which printsResults: X passed, Y failedand returns the process exit code.
New tests follow the existing idiom: extends SceneTree, _init() → _run.call_deferred(),
print PASS: / FAIL:, and quit(t.report()).
Next step: GdUnit4
The hand-rolled harness is enough for CI today. The plan's Workstream 5 adopts GdUnit4 for a fluent assertion API, its scene runner (synthetic input + frame stepping — purpose-built for the interaction layer), and HTML/JUnit reports. Adoption is incremental, so the current tests keep running throughout:
- Add it under
addons/gdUnit4(AssetLib in-editor, or a git submodule) and enable the plugin in Project Settings → Plugins. - Point CI at GdUnit4's own GitHub Action (it publishes the JUnit report to the PR) alongside the
existing
run_tests.shstep. - Port suites one at a time —
test_helpers.gd's primitives map directly onto GdUnit4 assertions, so a ported test is smaller, not larger.
Do not vendor GdUnit4 into this repo by hand; install it through AssetLib or a submodule.