Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
34a80ba
feat(runner): cyberpunk action-figure Hoppy with an athletic run
juanpflores Oct 9, 2026
b9f6662
feat(runner): shared Blender boss kit, dissolve shader and model loader
juanpflores Oct 9, 2026
032a6ce
fix(runner): dissolve bosses in rest space with a late-remnant curve
juanpflores Oct 9, 2026
fd73c5f
fix(runner): pace the boss dissolve so it burns instead of popping
juanpflores Oct 9, 2026
6906502
feat(runner): Blender-built Flaky Test Swarm boss
juanpflores Oct 9, 2026
021b62f
feat(runner): Blender-built Merge Conflict Hydra boss
juanpflores Oct 9, 2026
2bf2f4b
feat(runner): Blender-built Legacy Monolith boss
juanpflores Oct 9, 2026
e85e171
feat(runner): boss cinematics contract 2.6
juanpflores Oct 9, 2026
aaf561c
merge(runner): flaky-test-swarm boss model
juanpflores Oct 9, 2026
1bbb5fa
merge(runner): merge-conflict-hydra boss model
juanpflores Oct 9, 2026
d54c772
merge(runner): legacy-monolith boss model
juanpflores Oct 9, 2026
ad0b797
chore(runner): add the boss cinematic test uid
juanpflores Oct 9, 2026
e01fdb4
feat(runner): shared intro hold timing and per-boss camera focus
juanpflores Oct 10, 2026
a03521a
test(runner): check boss copy against the roster and accepted tools
juanpflores Oct 10, 2026
35d18b5
fix(runner): keep the Legacy Monolith inside the boss envelope
juanpflores Oct 10, 2026
2bfac15
merge(runner): Legacy Monolith envelope fix
juanpflores Oct 10, 2026
84e2db9
fix(runner): keep the Flaky Test Swarm inside the boss envelope
juanpflores Oct 10, 2026
d8bb64f
merge(runner): Flaky Test Swarm envelope fix
juanpflores Oct 10, 2026
14c6b75
fix(runner): keep the Merge Conflict Hydra inside the boss envelope
juanpflores Oct 10, 2026
9b206d8
merge(runner): Merge Conflict Hydra envelope fix
juanpflores Oct 10, 2026
4e50b14
fix(runner): pull the boss defeat close-up back to keep faces in frame
juanpflores Oct 10, 2026
b614075
fix(runner): finish the Legacy Monolith retreat before it is freed
juanpflores Oct 10, 2026
a33c433
merge(runner): Legacy Monolith retreat timing
juanpflores Oct 10, 2026
5763daa
feat(runner): Zelda-style boss title card and defeat sequence
juanpflores Oct 10, 2026
71bd711
polish(runner): aim the Hydra intro camera lower to fill the frame
juanpflores Oct 10, 2026
e4673e5
polish(runner): lead the swarm hint with its tell and tint defeat bursts
juanpflores Oct 10, 2026
d1ec448
merge(runner): Hydra framing polish
juanpflores Oct 10, 2026
4ab289d
fix(runner): harden the boss Blender tooling and rebake with fresh pr…
juanpflores Oct 10, 2026
b8df4e5
fix(runner): let defeat audio ring out and keep boss models warm
juanpflores Oct 10, 2026
bff9464
fix(runner): close zero-tail skips and forward boss health to visuals
juanpflores Oct 10, 2026
a616a46
docs(runner): fix the boss camera hook signature and intro test wording
juanpflores Oct 10, 2026
c04221a
fix(runner): show the skip prompt on short boss intros
juanpflores Oct 10, 2026
c5528b7
refactor(runner): expose a public boss skip query for the overlay
juanpflores Oct 10, 2026
9377124
perf(runner): validate boss rest positions once per mesh
juanpflores Oct 10, 2026
1042c09
refactor(runner): let tests override the Hydra model path
juanpflores Oct 10, 2026
cae766a
test(runner): remove throwaway boss leaderboards on every smoke path
juanpflores Oct 10, 2026
a061f48
fix(runner): fall back to procedural art for boss models without a sk…
juanpflores Oct 10, 2026
195c8bc
fix(blender): resolve unused imports in boss generation tools (#68)
coderabbitai[bot] Oct 10, 2026
cd85be7
Resolve merge conflicts in 22 file(s)
coderabbitai[bot] Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 119 additions & 12 deletions docs/endless-runner-contract.md

Large diffs are not rendered by default.

36 changes: 27 additions & 9 deletions docs/endless-runner-systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,16 @@ plus the optional `boss/boss_fx.tscn` frontend mount (contract §11.3, §11.8) t
phase) scaled by the cycle escalation (contract §11.10), with `boss_hp_scale` multiplying the base HP before that
growth so a test config can shorten a fight without bending the curve; `hp = max_hp`; `phase = 1`; mount the visual
(`ResourceLoader.exists` → `load as PackedScene` → set base vars → `add_child` → `on_spawned()`; graybox fallback: a
DANGER box 4×2.4×1.5 with a Label3D name); position `Boss` at `(0, 2.6, boss_z)`; `_intro = boss_intro_seconds`;
emit `boss_intro(kind, name, _intro)` **first** (the entrance card, contract §11.4), then `boss_spawned(kind, hp)`
and `boss_health_changed(hp, max_hp)`; `_attack_timer = attack_interval(phase)`;
`_tool_timer = tool_interval × 0.5`.
DANGER box 4×2.4×1.5 with a Label3D name); position `Boss` at `(0, 2.6, boss_z)`; `_intro = boss_intro_seconds` on
the run's first meeting with this kind, `min(boss_intro_short_seconds, boss_intro_seconds)` on a repeat (contract 2.6
§11.14; the per-run `_met` set, restored by `mark_met()` on a checkpoint retry, as `_defeated_kinds` is by `mark_defeated()`);
emit `boss_intro(kind, name, _intro)` **first** (the entrance card, contract §11.4), then
`boss_cinematic(kind, intro_full|intro_short, _intro)` (§11.14, skipped for a zero-length intro), then
`boss_spawned(kind, hp)` and `boss_health_changed(hp, max_hp)`; `_attack_timer = attack_interval(phase)`;
`_tool_timer = tool_interval × 0.5`. A cinematic rides the countdown it holds (`_intro`, or `_outro` for a defeat),
so `cinematic_progress()`, `can_skip_cinematic()`, `skip_cinematic()` (intros only: jump to the last `boss_cinematic_skip_tail`
seconds) and `time_factor()` all read the same clock; it closes with `boss_cinematic_ended` when that countdown
reaches 0, and `abort()` closes it early.

`tick(delta)`: intro countdown (no attacks) → ACTIVE: `_attack_timer -= delta`; when ≤ 0 pick the next beat of the
current pattern (patterns cycle a fixed beat list; lanes from `stage_rng` where the pattern says "random", never
Expand Down Expand Up @@ -605,10 +611,20 @@ Damage: `apply_tool(kind)` (called by `tool_pickup.collect`): `damage = ToolDefs
`visual.set_phase(phase)`, `_attack_timer = min(_attack_timer, 0.6)`; `hp == 0` → `_defeat()`. `_defeat()`: bonus
`= boss_kill_bonus_base + boss_kill_bonus_per_stage × (stage_index − 1)` added to score, emit
`boss_defeated(kind, bonus)`, `visual.play_defeat()`, clear queued telegraphs, `clear()` live projectiles (no dodge
credit), free live tools, `_outro = boss_outro_seconds`; `_retreat()`: bonus `= kill_bonus × boss_retreat_bonus_ratio ×
(1 − hp / max_hp)`, emit `boss_retreated`, `visual.play_retreat()`, same cleanup. After the outro `finished` is
set (a plain bool the StageManager polls), the boss node is freed. `abort()` (on caught) frees everything without
signals or bonuses.
credit), free live tools, `_outro` = the defeat cinematic (`boss_defeat_seconds` on the run's first defeat of the kind,
`min(boss_defeat_short_seconds, boss_defeat_seconds)` after), then `boss_cinematic(kind, defeat_full|defeat_short,
_outro)` on the same tick (contract 2.6 §11.14); `_retreat()`: bonus `= kill_bonus × boss_retreat_bonus_ratio ×
(1 − hp / max_hp)`, emit `boss_retreated`, `visual.play_retreat()`, same cleanup, `_outro = boss_outro_seconds` and no
cinematic. After the outro `finished` is set (a plain bool the StageManager polls), the boss node is freed. `abort()`
(on caught) frees everything without fight signals or bonuses; a live cinematic still closes with
`boss_cinematic_ended`.

StageManager side (§11.14): `boss_cinematic` sets `cinematic_hold` (player controls off through
`RunnerGame.refresh_player_controls()`, `speed_controller.set_cinematic_scale(boss_cinematic_speed_scale)`), and
`boss_cinematic_skipped` / `boss_cinematic_ended`, a catch, a reset and the stage clear release it. Every boss tick
drives `camera_rig.set_boss_cinematic(progress, mode, boss_global_position, focus_global_position)` (the focus is
`boss_encounter.cinematic_focus`, the visual's `cinematic_focus()` read once after `on_spawned()`) and publishes the facade mirrors
`boss_cinematic_mode`, `boss_cinematic_progress`, `boss_time_factor`. `_snapshot()` adds `bosses_met` and `bosses_defeated_kinds`.

Both endings can be reached from inside a physics callback — the killing blow is a tool auto-firing in
`area_entered`, and a catch aborts the fight the same way — so the arena **never** takes a projectile or a tool out
Expand Down Expand Up @@ -769,7 +785,9 @@ boss timeout → `boss_retreated` with the partial bonus and never `boss_defeate
camera back at zero, no spawn during the bend) plus `turn_incoming` once per seam ahead of `turn_started`, on the
same direction and with `seconds ∈ [0, turn_warning_seconds + 0.5]`; the camera swing profile (0 at p = 0 and
p = 1, at the peak across the hold window, monotone through each ramp); `boss_intro` exactly once per fight,
before `boss_spawned`, carrying `boss_intro_seconds`, with no telegraph and no tool inside that window;
before `boss_cinematic` and `boss_spawned`, carrying `boss_intro_seconds` on the run's first meeting with that boss and
`min(boss_intro_short_seconds, boss_intro_seconds)` on a repeat meeting or a checkpoint retry, with no telegraph and no
tool inside that window (contract 2.6: the cinematic timings themselves are covered by `tests/runner_boss_cinematic.gd`);
`pursuit_alert` ordering and the epsilon gate; caught mid-boss aborts
to `idle` with `run_finished` once and boss stats in the result; speed with stage bonus and difficulty floor.
`runner_smoke.gd`: duplicated config `stage_base_distance 260`, `time_to_max_speed 20`, `boss_hp_scale 0.4` (so the
Expand Down
40 changes: 39 additions & 1 deletion docs/endless-runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ Frontend-only implementation of contract 2.1, sections 11.4, 11.9 and 11.10. No
- Corridor yaw reaches 55 degrees at progress 0.3, holds through 0.7, then returns to zero at 1.0. Curvature peaks at 0.010. `TURN_SWING_IN`, `TURN_SWING_OUT` and `TURN_YAW` in `corridor.gd` mirror RunConfig defaults and must stay in sync with the backend camera. The end wall spans the corridor and carries a 120-unit directional TURN face. Glyphs sweep laterally, rain accelerates and speed lines intensify in the hold. Endpoints and explicit `(0, 0)` reset restore the same pose.
- The turn preview mirrors the backend camera swing: positive direction applies positive yaw and bank, orbiting the player at (0, 0, 0), with 55° yaw, 12° bank, and 0.3 ramp-in/out fractions. Hold alignment is checked in both directions at p = 0.3/0.5/0.7.
- `boss_intro(kind, name, seconds)` opens a top-centre card at x=300..980, y=48..128 at 1280x720. It displays `WARNING · BUG DETECTED`, the supplied boss name at 24 units, and a kind-specific taunt typed over one second. Copy is centralized in `boss_hud.gd:TAUNTS`; unknown kinds show only the supplied name. The stage banner stays suppressed, and the health HUD slides in only after the supplied duration. Pause freezes the card and entrance.
- Boss FX forwards the optional frontend `play_intro(seconds)` method to the mounted visual. It resolves `World/TurnRig/BossArena/Boss` first because the facade mirrors `boss` only at the subsequent `boss_spawned` signal; mock facades use the property fallback. Swarm tiles assemble from the vanishing point, Hydra rises and unfurls three heads, and Monolith approaches from the far end before a slam. A DANGER vignette and screen flash accent the impact at 80% of the intro. No frontend code calls camera shake. Every entrance frame keeps x within [-3.5, 3.5] and y within [1.6, 6.1]. Only depth travel toward the far end is allowed, never closer than hover_z + 3. Swarm tiles converge along -Z, Hydra unfolds from a compressed above-floor pose, and Monolith slides along -Z. Transforms reset at the duration boundary.
- (Superseded in contract 2.6: `boss_fx` no longer forwards `boss_intro`, and the encounter's `play_cinematic(intro_*)` is the only intro cue in a run. `play_intro` remains for standalone and dev fixtures.) Boss FX forwards the optional frontend `play_intro(seconds)` method to the mounted visual. It resolves `World/TurnRig/BossArena/Boss` first because the facade mirrors `boss` only at the subsequent `boss_spawned` signal; mock facades use the property fallback. Swarm tiles assemble from the vanishing point, Hydra rises and unfurls three heads, and Monolith approaches from the far end before a slam. A DANGER vignette and screen flash accent the impact at 80% of the intro. No frontend code calls camera shake. Every entrance frame keeps x within [-3.5, 3.5] and y within [1.6, 6.1]. Only depth travel toward the far end is allowed, never closer than hover_z + 3. Swarm tiles converge along -Z, Hydra unfolds from a compressed above-floor pose, and Monolith slides along -Z. Transforms reset at the duration boundary.
- Audio has 32 cached recipes. `turn_warning` contains three rising ticks scaled to the warning window. The hard-panned turn whoosh includes its snap at the 0.3 hold boundary. `boss_intro` is a deep rising drone and hit, scaled to the intro window. Intro music ducks by 12 dB, releasing into the existing 4 dB boss duck. The seven polyphonic plus three dedicated voice allocation remains capped at ten.
- The mock demo emits both new signals, mounts its boss visual, and delays attacks until the three-second intro has elapsed. Smoke coverage includes `TURN WARNING: PASS` and `BOSS INTRO: PASS`, directional warnings, countdown, correct taunts, fallback copy, visual forwarding/poses, HP handover, banner suppression, pause, audio duck release and the revised turn profile.
- Graphical capture fixtures are registered as `endless-runner-turn-warning.png` and `endless-runner-boss-intro.png`. The coordinator captured both PNGs on the host. Existing capture checks retain the corridor budgets of 14 at rest and 18 during a turn.
Expand Down Expand Up @@ -984,3 +984,41 @@ The standard backend smoke reported `RUNNER SMOKE: FAIL (3 failures / 127 checks
### Performance: W1 world motion clock

One scroll clock: the spawner keeps `scroll_travel` / `scroll_travel_previous` (metres scrolled at the latest and the previous physics tick). A bound corridor (`corridor.gd` `bind()`) draws its floor, walls, racks and the starship world at `lerp(previous, latest, Engine.get_physics_interpolation_fraction())`, which is exactly where the interpolated chunk, hazard and pickup transforms are drawn, so floor markers no longer swim against the grid. Unbound corridors (runner menu, fixtures) keep their render-delta clock. The turn bend is interpolated the same way: `set_turn()` calls made inside a physics tick keep the previous tick's signed wave and progress, and `_apply_turn()` draws between them like the interpolated camera swing; calls from outside physics snap. SpeedLines emission has one writer (`_apply_turn()`, changed-only, never on a hidden emitter), and the rain/streak process materials are rewritten only when the speed moves by 0.05 m/s. Each placed chunk calls `reset_physics_interpolation()` after its gap and pickup placement, so it never slides in from `frontier_z` for a frame. Interpolation opt-outs on cosmetic children are consistent with this: a measured `PHYSICS_INTERPOLATION_MODE_OFF` child renders at its interpolated parent's pose (the chunk scroll stays interpolated); OFF only stops interpolation of the child's own local transform. Cameras (game and menu) use near 0.15 / far 200 for 3x the depth precision of the 0.05 default; the farthest geometry (deck plane, fog end at 96 m, boss entrance at -69 m) stays well inside. The pursuer light is written only on a 0.01 energy change, and a retreat requested while the threat is still gliding down is ignored (the next tick cancelled it anyway, after a one-frame light-off flash). Measured and left alone: chunk despawn (remove + free of the largest chunk, 229 nodes, is 0.3 ms rendered) and the per-tick `runner_hazards` group scans (about 0.1 µs each). The starship corridor now leaves the legacy `KeyLight` at its palette tuning and switched off, and lights the deck with an identical `StarshipKeyLight` in the same place; every StarshipWorld part is PAUSABLE, like the rest of the corridor. Measured in a rendered turn, corridor yaw now matches the interpolated camera swing to 0.000° (the latest-tick yaw it used before was up to 3.6° ahead).

## Contract 2.6: boss cinematics (backend hand-off)

This section mirrors contract §11.14 for the frontend; the contract is authoritative. The backend owns the clock, the
camera, the player hold and the skip, and the frontend draws from that clock.

- **What the backend sends.** Intro: `boss_intro(kind, name, s)`, `boss_cinematic(kind, mode, s)`, `boss_spawned`,
`boss_health_changed`. The `mode` is `intro_full` (5.0 s, the run's first meeting with this boss) or `intro_short`
(2.5 s, every repeat meeting and every checkpoint retry). Defeat: `boss_defeated(kind, bonus)`, then
`boss_cinematic(kind, defeat_full | defeat_short, s)` on the same tick (3.6 s first defeat, 2.0 s repeat), and
`stage_cleared` only once it is over. Each one ends with exactly one `boss_cinematic_ended(mode)`. Skipping an intro sends
`boss_cinematic_skipped(mode)` first and still plays the last 0.3 s. A defeat is never skippable. A retreat has no cinematic and keeps its
1.0 s outro.
- **What to draw from.** `game.boss_cinematic_mode`, `game.boss_cinematic_progress` (0..1) and `game.boss_time_factor`.
All three freeze on pause and jump on a skip, so `ui/boss_cinematic.tscn` should hold no Tweens or timers of its
own. The backend mounts `res://scenes/runner/ui/boss_cinematic.tscn` under `HUD` once it exists, like the other
optional panels (PAUSABLE, idempotent `has_signal`-guarded `bind(game)`).
- **Time factor.** It is 0.05 through the defeat's 0.12 s hit-stop, then eases from 0.4 back to 1 over 0.8 s, and is
1 everywhere else. Multiply it into your own delta. `Engine.time_scale` is never touched.
- **Boss visual hooks** (optional, `has_method`-guarded): `play_cinematic(mode, seconds)` once, right after
`on_spawned()` / `play_defeat()`, as the set-up cue. Then `set_cinematic_progress(mode, progress)` with 0, on every
physics tick of the cinematic, once after a skip with the jumped progress, and with 1.0 at the end. Pose, dissolve,
flash and strobe come from that `progress`: seek the action to progress-mapped time, never an accumulated clock.
`set_time_factor(factor)` arrives with it and scales only ambient motion (idle sway, particles) during the slow-mo
tail. In a run, `play_cinematic(intro_*)` is the only intro cue: `boss_fx` no longer forwards `boss_intro` to `play_intro(seconds)`, which had started the intro twice. `play_intro` stays an optional hook for standalone and dev fixtures. After a kill the visual is freed when
the defeat cinematic ends.
- **Intro beat.** `boss_visual_base.gd` `INTRO_POSE_AT` (`intro_full` 0.40, `intro_short` 0.28): land the final intro
pose by this progress and slam the title card at the same moment. Hold the pose frozen until `INTRO_HOLD_UNTIL`
(0.66 / 0.88), where the camera leaves the hold. Read the constants; don't copy the numbers.
- **Camera aim.** Optional `cinematic_focus() -> Vector3` (visual-local) is read once after `on_spawned()`. The default
is the centre of the upper third of the rest mesh bounds. Override it to aim at a head or a core.
- **Camera.** The camera is backend-owned (`camera_rig.boss_shot(p, mode)`): a low-angle push with a freeze-hold and a
wide hint shot for the intro, and a punch-in, ±12° orbit and wide pull-back for the defeat. Frontend code does not
move the camera. It may still call `shake()` and `punch_fov()`, which ride on top of the shot.
- **Skip.** Intros only. Jump or `ui_accept` skips once 0.5 s have played (`skip_boss_cinematic()`). The press is
consumed and never jumps. Show the skip prompt only while `game.can_skip_boss_cinematic()` returns true; don't
derive it from elapsed time. The backend guard already returns false while paused or not running, once the intro
has been skipped, inside its last 0.3 s tail (`boss_cinematic_skip_tail`), before 0.5 s, and for every defeat.
1 change: 1 addition & 0 deletions hoppys-adventure-arcade/Assets/runner/LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ No external downloads. F5 reuses the existing bundled music package.
| Procedural runner SFX | scenes/runner/audio/sfx_synth.gd | Project contributors | Original deterministic PCM recipes | Project source terms; no third-party asset licence | Original |
| Cyberpunk Street BGM | res://Assets/cyberpunk-street-files/Assets/music/cyberpunk-street.ogg | Luis Zuno / ansimuz | Assets/cyberpunk-street-files/public-license.pdf | CC0 | none; the PDF does not name the music's composer |
| Blender Hoppy mascot | Assets/runner/hoppy/** | Project contributors | generated in-repo by tools/blender/build_hoppy.py (Blender 5.2.2) | Project licence | Original work |
| Hydra conflict-marker glyphs | Assets/runner/bosses/merge_conflict_hydra/** | DejaVu fonts contributors / Bitstream | Blender bundled DATAFILES/fonts/DejaVuSansMono.woff2 | [Bitstream Vera and DejaVu Fonts License](https://dejavu-fonts.github.io/License.html); font name and SHA-256 recorded in build.json | Converted <<<<<<<, ======= and >>>>>>> outlines to mesh and baked atlas; no font file redistributed |
Loading
Loading