diff --git a/crates/skate-data/examples/hud_data.rs b/crates/skate-data/examples/hud_data.rs index 4740079d..747046d7 100644 --- a/crates/skate-data/examples/hud_data.rs +++ b/crates/skate-data/examples/hud_data.rs @@ -38,6 +38,9 @@ fn main() -> Result<(), String> { serde_json::from_value(json["shapes"].clone()).map_err(|e| e.to_string())?; let mut max_draws = 0; let mut saw_native_shadow_pair = false; + let mut glow_alphas = std::collections::BTreeSet::new(); + let mut flicker_alphas = std::collections::BTreeSet::new(); + let mut previous_timer_frame = None; for tick in 0..1800 { let new_trick = tick % 180 == 10; if new_trick { @@ -54,7 +57,11 @@ fn main() -> Result<(), String> { input.clean = tick % 360 == 10; input.sketchy = !input.clean; input.line_time = 8.0; - input.multiplier = if input.multiplier == 1.5 { 2.0 } else { 1.5 }; + input.multiplier = match (tick / 180) % 3 { + 0 => 1.5, + 1 => 2.0, + _ => 3.0, + }; } input.line_time = (input.line_time - 1.0 / 60.0).max(0.0); input.sequence_timer = input.line_time as i32; @@ -69,6 +76,34 @@ fn main() -> Result<(), String> { } } let draws = apt_scene::draw(&runtime.bindings.movie, &runtime.vm, &shapes)?; + for (&id, instance) in &runtime.bindings.movie.instances { + let name = instance.placement.as_ref().map(|p| p.name.as_str()); + if name == Some("multiTimer_mc") && (20..40).contains(&tick) { + if let Some(previous) = previous_timer_frame { + if instance.frame != previous + 1 { + return Err( + "Line timer was re-seeked instead of playing consecutive frames".into(), + ); + } + } + previous_timer_frame = Some(instance.frame); + } + if input.multiplier == 3.0 { + let alpha = runtime.vm.get(id, "_alpha").number(); + if name == Some("mBlueGlow") { + if !(80.0..100.0).contains(&alpha) { + return Err("Blue glow outside authored flicker range".into()); + } + glow_alphas.insert(alpha.to_bits()); + } + if name == Some("mFlicker") { + if !(90.0..100.0).contains(&alpha) { + return Err("Multiplier flicker outside authored range".into()); + } + flicker_alphas.insert(alpha.to_bits()); + } + } + } max_draws = max_draws.max(draws.len()); saw_native_shadow_pair |= draws.windows(2).any(|pair| { pair[0].texture.contains("futurashadow") @@ -88,6 +123,9 @@ fn main() -> Result<(), String> { if !saw_native_shadow_pair { return Err("Missing native black shadow / heavy foreground pair".into()); } + if glow_alphas.len() < 4 || flicker_alphas.len() < 4 { + return Err("Original x3 blue glow/flicker clips did not animate".into()); + } println!( "Original HUD action audit passed 1800 authored frames; {} object slots, {} display instances", runtime.vm.objects.len(), diff --git a/crates/skate-game/src/hud_runtime.rs b/crates/skate-game/src/hud_runtime.rs index 66da1da8..50f4017f 100644 --- a/crates/skate-game/src/hud_runtime.rs +++ b/crates/skate-game/src/hud_runtime.rs @@ -236,13 +236,20 @@ impl Runtime { .call_method(self.controller, method, vec![], &mut self.bindings)?; self.drain()?; } - self.vm.call_method( - self.controller, - "UpdateLineDisplay", - vec![], - &mut self.bindings, - )?; - self.drain()?; + // The movie handles this as event 11 (0196CC), not onEnterFrame. + // Re-seeking the 501-frame multiTimer_mc on every tick pins it to + // the same whole-second sample instead of letting gotoAndPlay run. + if previous.line_time.trunc() != self.bindings.input.line_time.trunc() + || previous.line_score != self.bindings.input.line_score + { + self.vm.call_method( + self.controller, + "UpdateLineDisplay", + vec![], + &mut self.bindings, + )?; + self.drain()?; + } self.bindings.movie.advance(&mut self.vm)?; self.drain()?; self.vm diff --git a/docs/native-scoring-hud.md b/docs/native-scoring-hud.md index d25899b9..c80c8fc1 100644 --- a/docs/native-scoring-hud.md +++ b/docs/native-scoring-hud.md @@ -98,8 +98,8 @@ The copied build and launcher are in ignored `logs/scoring/build`. The launcher uses this worktree's HUD cache and the existing owned asset installation and starts paused. It checks required paths before running and retains failures on screen. The executable SHA256 is -`6853a0035cb3a8693681e574f1fae5829f2caec694657536b92217c6d8f7c63b`. -The launcher now selects `skate3rust-hud-score-fix.exe`. +`59c7c876d289c58e9661f669274fa4faa4e50a7bf51085461bcf33f2fc1bd809`. +The launcher now selects `skate3rust-hud-animation.exe`. The missing-HUD startup defect is fixed: HUD setup now depends on presentation setup, so Bevy applies the deferred camera spawn before the HUD queries it. @@ -188,7 +188,31 @@ preparation order. An asset-only regression test checks that an unchanged target emits no Modified event and a resize invalidates both image and material. The test uses no window, renderer or gameplay systems. -## Native parity gaps +## Multiplier timeline playback + +The host previously called `UpdateLineDisplay` every fixed tick. The shipped +action at 019C39 computes `floor(GetLineTimeRemaining() * 60) - 30`, clamps +it, and seeks `multiTimer_mc` with `gotoAndPlay` at 019CF4. Since the native +binding returns whole seconds, repeated calls pin the 501-frame timer to the +same frame for a second, then jump it forward. The ActionScript dispatches +this method for event11 (0196CC) and ScreenShow, rather than onEnterFrame. +The adapter now dispatches when the exposed timer sample or line score changes, +allowing the authored timeline to advance between samples. This is an adapter +event policy; the entire native HUD event producer is not yet ported. + +Original x3 effect clips are retained: character59 changes mBlueGlow alpha +with `80 + Math.random() * 20` every four frames; character70 changes mFlicker +with `90 + Math.random() * 10`. Original multiplier labels select the glow +and background timelines (characters66 and77); no replacement particles, +colors or universal x3 effect were added to lower multiplier levels. + +The supplied-data movie audit now visits x1.5, x2 and x3 over 1,800 frames, +asserts consecutive timer frames between events, checks both flicker ranges +and changing alpha values, and reaches 40 authored draw batches. The scoring +flow audit still passes landing persistence, line expiry and cancellation. +No game or GPU capture was launched. + +## Remaining native parity gaps These are implementation gaps, not merely missing gameplay validation: