Siim reported gamepad menu navigation works but pressing a button
does nothing. Checked Godot's actual built-in ui_accept bindings
directly: only Enter/Kp Enter/Space — no joypad button at all. That's
why D-pad/stick focus movement (ui_left/right/up/down do default to
gamepad input) worked while "confirm" never could, on any controller.
Binds both the Xbox-style bottom face button (A, index 0) and the
right-hand one (B, index 1 — physically labelled "A" on a
Nintendo-layout pad, since gamepad mapping normalizes to Xbox
*position* rather than the printed label), so either common "confirm"
button activates the focused menu/end-screen button.
Verified headlessly end-to-end: a synthetic joypad button 0 or button
1 press on the focused end-screen button now reloads the run, matching
what a real gamepad press should do.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
Clearing the arena's last wave set the DP debug flag
"gate_unlocked_bear" to true as a side effect. That flag lives on the
DP autoload, which survives scene reloads and isn't touched by
Run.start_new_run() — so once cleared for real in a play session, it
stayed true forever, permanently overriding the per-run
Run.cleared-based gate check in lobby_underground.gd. Every later
lobby load (including after Play Again reset the real run state) kept
re-opening the gate.
The DP flag is meant to be a manual designer override for testing the
animation (per its own doc comment), not something gameplay code sets
persistently — Run.bear_gate_open() is already the real per-run source
of truth. Removed the stray set_value() call.
Verified in isolation: clearing the arena's last wave no longer
touches the DP flag (stays false), so the gate only opens through the
run-scoped Run.cleared state, which already resets correctly on Play
Again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
Nothing had focus by default when the outcome screen appeared, so a
gamepad's ui_accept/ui_left/ui_right (bound to Godot's built-in ui_*
actions) had no control to act on — same reason main_menu.gd grabs
focus on its own default button per panel. Grab it on whichever
button is visible (Play Again/Continue on a win, Retry on a loss);
Godot's geometry-based focus neighbors then let ui_left/ui_right reach
Main Menu from there. Verified headlessly: a synthetic ui_accept on
the focused button reloads the run, same as a real gamepad A press
would.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
Siim reported the hit-vibration works but the lobby gate's rumble
doesn't. Godot's start_joy_vibration() docs call this out directly:
long single effects are unreliable on much gamepad hardware/drivers,
and recommend a short effect repeated instead. Added
Controls.vibrate_for() to do that and pointed the gate at it in place
of its single 7-second call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
- The finish screen's continue button now reads "Play Again" and starts
a fresh run instead of "Continue"-ing to a lobby gate that no longer
exists past the run's last fight.
- The "Your Time" readout moves from the top (overlapping the bull in
thankyou.ogv) to just under the clip's own baked title, restyled to
match its flat dark-ink look instead of the gold HUD outline, and
pulses on the same ~1.4s breathing cycle sampled off the source clip
so it reads as part of the poster rather than a readout stamped on it.
- Added a `video <win|loss|finish>` debug console command to preview
any outcome screen without playing through a fight, via a new
hud.gd debug entry point (`debug_force_outcome`).
- Dropped the console's per-frame "focus stolen by ... — reclaiming"
log line — its own comment flagged it as temporary diagnostic
instrumentation, and it fired on every console open.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
Input.start_joy_vibration() is a documented engine no-op on Godot's
HTML5 export (never wired to the browser side). Controls.vibrate()
wraps it: native platforms go straight through as before, web drives
the browser's Gamepad Haptic Actuator API directly via JavaScriptBridge.
Gamepad movement/ability input already worked on desktop browsers via
the standard Web Gamepad API, so only vibration needed a fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
Defeating the bear (the run's final boss) now plays a dedicated
"Thank You For Playing" clip on the completion screen instead of the
generic bull-win video, with the run's finish time shown in place of
the usual "Bull Won!" title.
A run clock also starts when the player enters the arena (the run's
first fight), shown top-left on desktop only, and freezes the instant
the bear falls. Re-entering the arena after finishing a run restarts
the clock instead of leaving it stuck on the previous run's time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
Move every script and scene into role-based directories (actors/, systems/, ui/,
levels/, props/) and all raw art under art/ (grouped by role, snake_case,
Extract-Textures GLBs), dropping Blender version-suffix noise. Filenames are
snake_case (PascalCase reserved for class_name) to avoid case-insensitive
filesystem aliasing on the web/macOS targets. Routes the bull's hits through the
typed DamageEvent pipeline and wires the bull's abilities to the Fx library.
Updates CLAUDE.md's folder-layout overview to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
An Fx autoload + one_shot_effect base so every effect is an inspector-editable
.tscn (the systems/fx/ authoring dir): blood/slam/spark/flash primitives and the
bull's kick/dash/slam/roll + movement effects (bull_*.tscn), fired via
Fx.spawn() / Fx.attach(). Plus a preview gallery + smoke test, and
bull_impact_fx.gd for the coded, non-particle slam rings + kick flash-pop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
A typed DamageEvent contract (systems/combat/damage_event.gd) as a single
entry point for every attacker, plus a characterization test locking the bull's
damage reception. Attacker/receiver wiring rides along with the moved actor
scripts in the reorg commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
A Gitea Actions workflow (.gitea/workflows/ci.yml) running the headless test
suite on push, plus shared test scaffolding: seeded-RNG / approx / tally helpers
(test_helpers.gd), a harness self-test, and a resolution + pseudo-locale
UI-layout robustness guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
Strip only the floor/wall blood-splat decals added in 981ebf1, keeping the
older death-spray, spear-wound spurt, and "Blood & Gore" toggle:
- Delete blood_decals.gd + shader, the Gore autoload (gore.gd), and
tests/blood_decals_test.gd
- Drop the Gore autoload and the Blood DP section
- Remove all Gore.splat() call sites (matador.gd, bear.gd, player.gd);
the _blood_burst spray and _spawn_blood spurt stay
- game_shell no longer spawns a per-level decal pool
- Update level_switch test + run_tests.sh accordingly
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
Bull's boulder roll now homes on and ploughs through lobby barrels, and
the slam shockwave bursts them within its radius. HUD tallies barrels on
load and fires a confetti "WOooOoW!" + two gold bonus HP pips when the
last one breaks; gates open from run state as fights are cleared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bull charge damage:
- Tunnel-proof swept detection (prev→now segment) for matador gore and bear
ram, so a 66 m/s dash no longer skips clean over the thin HitArea.
- Horns-first cone (bull_gore_arc): running into an enemy only wounds it when
charging roughly at it, not a sideways brush.
- Dash is a committed lunge: bypasses the horns cone AND the survival roll, so a
dash connect is a guaranteed kill. Cruise charges keep a speed-scaled roll
(mat_charge_pierce). Roll ability defers to its own pop (is_rolling guard).
Bear:
- Horns-first swept ram detection (edge-triggered: one charge = one wound);
a tunnelling or point-blank charge now lands where body_entered wouldn't.
- Overhead smash lane extends faster (0.1s) and further (20 m).
- Leap slam launches the bull from anywhere in the circle (ring_frac 0).
- Removed stray-quote syntax errors that broke the script.
Matador:
- Collider-driven stab (BladeHitbox) with recovery beat + taunt-after-hit so it
stops machine-gunning the bull.
- A taunting matador answers a charge: spear from range, sword up close.
Spear projectile masks the BULL layer so throws connect.
Tests: matador_charge/stab/spear/taunt_react, bear_ram/hitbox/fx (+ unified
_fake_bull stand-in). Trimmed verbose comments.
Note: bear claw-emitter/death work is still WIP (3 bear_boss_test failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A killed matador's ragdoll shared the WORLD layer with the bull, so the
bull collided with the bodies and spazzed out on them. Give corpses a
dedicated CORPSE layer masked to itself, and tag the ground/walls with
CORPSE too (at level load, so it also reaches colliders baked into the
instanced GLB arenas). The corpse still rests on the arena, but the bull
— which only masks WORLD — passes straight through the bodies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>