From b2f9f96db9364440ec94a97c7e01e16542964960 Mon Sep 17 00:00:00 2001 From: Erik Thorelli Date: Fri, 9 Oct 2026 21:02:37 -0700 Subject: [PATCH] test(runner): make the A6 and A8 asset checks follow #61's held catch [Agent: Claude Code | Model: Opus 5.5 | Thinking: High | Acting on behalf of: Erik | Skill: none] #61 changed the pursuer's catch: the drone now leaps past Hoppy, clamps its claws at CLAMP_AT (1.6 s), drops a world-space quarantine field and holds while the threat stays at 1. It also raised caught_sequence_seconds from 1.6 to 3.0. The asset checks, which CI didn't run, still expected the old catch, and both had failed since (#65). A6 leaves the quarantine field out of the hunter envelope, checks the claws at CLAMP_AT, and asserts the leap past Hoppy without scaling. It also adds three checks: the field drops once the claws shut, the catch holds at threat 1, and a run reset ends it. The retreat pose comes from pursuit, not from the held catch. A8 runs the mock timeline until the demo finishes instead of for a fixed 405 ticks, so the Game Over sting still fires whatever the configured catch length, and it asserts that the timeline reached Game Over. The docs' A6 and A8 rows quote the current results. Closes #65. --- docs/endless-runner.md | 4 ++-- .../tests/assets/a6_check.gd | 20 +++++++++++++++---- .../tests/assets/a8_check.gd | 8 ++++++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/endless-runner.md b/docs/endless-runner.md index 7d8b2d4f..3129430a 100644 --- a/docs/endless-runner.md +++ b/docs/endless-runner.md @@ -78,9 +78,9 @@ Paths beginning with `tests/` or `scenes/` are relative to `hoppys-adventure-arc | A3: Hazards + projectiles | Shared relief atlas, forged meshes, pit-depth and exposure guards | Maximum 11 / 5,968 per asset; 3 shared 512² maps + 256² border | `--script res://tests/assets/a3_check.gd`; `ASSET A3: PASS (158393 checks)` | `scenes/runner/hazards/dev/capture_a3.gd` | `docs/previews/assets/a3-*.png` | [A3](assets/a3.md) | | A4: Power-ups + tools | Upward beacons, soft rings, distance-aware glyphs, bounded body emission and grab-vs-avoid affordances | 7 idle, 8 peak each / up to 2,196 peak triangles / shared relief maps and per-kind glyph textures | `--script res://tests/assets/a4_check.gd`; `ASSET A4: PASS (6035 checks)` | `scenes/runner/powerups/dev/capture_a4.gd` | `docs/previews/assets/a4-*.png` | [A4](assets/a4.md) | | A5: Bosses + fight FX | Forged reactor, articulated Hydra, cached metal maps, pooled telegraphs/tracers/numbers | Swarm 11 / 22,080; Hydra 18 / 21,396; Monolith 14 / 6,970; FX pool 22 / 7,716; whole fight 40 draws / 4 shared 256² textures | `--script res://tests/assets/a5_check.gd`; `ASSET A5: PASS (60339 checks)` | `scenes/runner/boss/dev/capture_a5.gd` | `docs/previews/assets/a5-*.png` | [A5](assets/a5.md) | -| A6: Pursuer | Bevelled MultiMesh PR cards, Sobel normals, routed cables, threat-driven effects | 13 / 7,144 / 3 × 256² | `--script res://tests/assets/a6_check.gd`; `ASSET A6: PASS (102 checks)` | `scenes/runner/pursuer/dev/capture_a6.gd` | `docs/previews/assets/a6-*.png` | [A6](assets/a6.md) | +| A6: Pursuer | Robotic QUARANTINE hunter: two claws, receivers, lift rotors and a red light; the catch leaps past Hoppy, clamps at 1.6 s and drops a containment field | 12 / 3,840 / none | `--script res://tests/assets/a6_check.gd`; `ASSET A6: PASS (209 checks)` | `scenes/runner/pursuer/dev/capture_a6.gd` | `docs/previews/assets/a6-*.png` | [A6](assets/a6.md) | | A7: UI | GET READY and menu grab-vs-avoid legend, first-event toasts, SPEED handoff, vector panels/icons and portrait viewport | 0 3D draws / 0 mesh tris / 0 procedural maps; 217 CanvasItems and existing 256² portrait viewport (not zero UI GPU cost) | `--script res://tests/assets/a7_check.gd`; `ASSET A7: PASS (1245 checks)`; `HUD LEGIBILITY: PASS (60 labels checked)` in visual smoke | `scenes/runner/ui/dev/capture_a7.gd` | `docs/previews/assets/a7-*.png` | [A7](assets/a7.md) | -| A8: Audio | Deterministic layered PCM, FM transients, filtered noise, family envelopes, leased reverb/compressor/delay buses | 0 / 0 / 0; 32 recipes, 3,901,938 checked PCM bytes, peak 0.789972 | `--script res://tests/assets/a8_check.gd`; `ASSET A8: PASS (2323 checks)` | `scenes/runner/audio/dev/capture_a8.gd` | `docs/previews/assets/a8-*.png` | [A8](assets/a8.md) | +| A8: Audio | Deterministic layered PCM, FM transients, filtered noise, family envelopes, leased reverb/compressor/delay buses | 0 / 0 / 0; 32 recipes, 3,901,938 checked PCM bytes, peak 0.789972 | `--script res://tests/assets/a8_check.gd`; `ASSET A8: PASS (2333 checks)` | `scenes/runner/audio/dev/capture_a8.gd` | `docs/previews/assets/a8-*.png` | [A8](assets/a8.md) | | A9: Post-processing | Single screen-reading shader: grading, grain, vignette, hit aberration, letterbox, speed streaks, Nitro lift | 1 fullscreen pass / 2 / 0 generated textures; implicit screen backbuffer copy is additional storage/bandwidth | `--script res://tests/assets/a9_check.gd`; `ASSET A9: PASS (147 checks)` | `scenes/runner/ui/dev/capture_a9.gd` | `docs/previews/assets/a9-*.png` | [A9](assets/a9.md) | A4 peak census includes the collection burst; code_review has the highest triangle count. A5 whole-fight draws are a concurrent budget, not the sum of every boss. A9's current 147-check verification supersedes both the earlier 126- and 134-check transcripts. diff --git a/hoppys-adventure-arcade/tests/assets/a6_check.gd b/hoppys-adventure-arcade/tests/assets/a6_check.gd index 359984c5..bc5e93c0 100644 --- a/hoppys-adventure-arcade/tests/assets/a6_check.gd +++ b/hoppys-adventure-arcade/tests/assets/a6_check.gd @@ -8,10 +8,13 @@ func check(ok: bool, message: String) -> void: failures += 1 push_error(message) func _initialize() -> void: call_deferred("run") +## The hunter rig's bounds. The quarantine field is a separate world-space effect dropped around +## Hoppy during a catch, so it is not part of the hunter's envelope. func bounds(node: Node3D) -> AABB: var result := AABB() var first := true for mesh: MeshInstance3D in node.find_children("*","MeshInstance3D",true,false): + if mesh == node.get("_field"): continue var box: AABB = node.global_transform.affine_inverse()*mesh.global_transform*mesh.mesh.get_aabb() result = box if first else result.merge(box) first = false @@ -46,13 +49,22 @@ func run() -> void: visual._process(0.1) check(not visual._fans[0].rotation.is_equal_approx(rotor),"Visible propulsion rotates") var count := visual.find_children("*","",true,false).size() + var start_z: float = visual._wall.global_position.z visual.play_catch() - visual._process(0.4) - check(absf(visual._arms[0].rotation.x) > 0.3,"Capture deploys claws") + visual._process(visual.CLAMP_AT) + check(absf(visual._arms[0].rotation.x) > 0.3,"Capture clamps the claws at CLAMP_AT") + check(visual._field.visible,"Quarantine field drops once the claws shut") visual.play_catch() - check(is_equal_approx(visual._reaction_time,0.4),"Catch idempotent") + check(is_equal_approx(visual._reaction_time,visual.CLAMP_AT),"Catch idempotent") visual._process(1.0) - check(visual._wall.scale.is_equal_approx(Vector3.ONE) and visual._wall.position.z > 2,"Catch lunges without giant growth") + var held: Transform3D = visual._wall.global_transform + var unscaled := held.basis.get_scale().is_equal_approx(Vector3.ONE) + check(unscaled and held.origin.z < start_z - 4.0,"Catch leaps past Hoppy without giant growth") + visual.set_threat(1) + check(visual._reaction == &"catch","Catch holds while the threat stays at 1") + visual.set_threat(0) + check(visual._reaction == &"idle" and not visual._field.visible, + "Run reset ends the catch and lifts the field") visual.set_threat(1) var pose: Transform3D = visual._wall.transform visual.set_threat(0) diff --git a/hoppys-adventure-arcade/tests/assets/a8_check.gd b/hoppys-adventure-arcade/tests/assets/a8_check.gd index 9d838354..fe9c7bc7 100644 --- a/hoppys-adventure-arcade/tests/assets/a8_check.gd +++ b/hoppys-adventure-arcade/tests/assets/a8_check.gd @@ -170,8 +170,11 @@ func run() -> void: audio.play_ui_click() var heard := {} audio.sound_played.connect(func(kind: StringName, _stream: AudioStream) -> void: heard[kind] = true) - # Existing mock timeline exercises all 32 signal-triggered recipes. - for i in 405: + # The mock timeline exercises all 32 signal-triggered recipes. Run it until it finishes, so the + # Game Over sting still fires when config.caught_sequence_seconds changes (#61 made it 3.0 s). + var ticks := 0 + while mock.state != &"finished" and ticks < 600: + ticks += 1 audio.one_shots.stop() audio.one_shots.play() audio._playback = audio.one_shots.get_stream_playback() @@ -180,6 +183,7 @@ func run() -> void: mock.advance_demo(0.1) audio._process(0.1) check(audio.active_voice_count() <= 10, "voice cap") + check(mock.state == &"finished", "demo timeline reaches Game Over (%d ticks)" % ticks) for kind: StringName in Synth.RECIPES: check(heard.has(kind), "trigger exercised: " + kind) audio._stop_all() audio.one_shots.play()