From e0b119d7290cf7732c89131aba3bc5c126c451dd Mon Sep 17 00:00:00 2001 From: chazmm Date: Mon, 7 Sep 2026 22:03:00 +0100 Subject: [PATCH] Connect native stalled grind release phase and outward exit forces --- crates/skate-core/src/physics/grind_forces.rs | 38 +++++++++++++++++ crates/skate-game/src/physics/grind.rs | 42 +++++++++++++++++++ docs/grind-release.md | 31 ++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 docs/grind-release.md diff --git a/crates/skate-core/src/physics/grind_forces.rs b/crates/skate-core/src/physics/grind_forces.rs index 7e2b9045..ebfadca1 100644 --- a/crates/skate-core/src/physics/grind_forces.rs +++ b/crates/skate-core/src/physics/grind_forces.rs @@ -164,3 +164,41 @@ fn cross(a: V, b: V) -> V { 0.0, ] } + +/// Grind PreUpdate82D3F4E8 / exit decision82D40DA0. Counts are native +/// simulation updates, not render frames. Once leaving, the phase is latched. +#[derive(Default)] +pub struct Release { + updates: u32, + leaving_updates: u32, + pub leaving: bool, +} +impl Release { + pub fn begin_update(&mut self, speed: f32, explicit_exit: bool) { + self.leaving |= explicit_exit || (self.updates > 60 && speed < 0.15); + } + pub fn finish_update(&mut self) -> bool { + self.leaving_updates = if self.leaving { self.leaving_updates.wrapping_add(1) } else { 0 }; + self.updates = self.updates.wrapping_add(1); + self.leaving_updates > 35 + } +} + +///82D3F850: push away from the contacted edge until the native lateral +///speed limit is reached. Ledge engagement may supply its outward direction. +pub fn release_force( + position: V, point: V, direction: V, normal: V, velocity: V, + surface_kind: u32, surface_side: V, force_across: bool, + strength: f32, speed_limit: f32, lift: f32, +) -> V { + let outward = if !force_across && surface_kind != 0 { + scale(surface_side, -1.0) + } else { + let across = cross(direction, normal); + scale(across, if dot3(across, sub(position, point)) > 0.0 { 1.0 } else { -1.0 }) + }; + if dot3(velocity, outward) >= speed_limit { return [0.0; 4]; } + let lateral = scale(outward, strength); + let upward = normal[1].max(0.0) * lift; + core::array::from_fn(|i| normal[i].mul_add(upward, lateral[i])) +} diff --git a/crates/skate-game/src/physics/grind.rs b/crates/skate-game/src/physics/grind.rs index 08b19ee1..98da1490 100644 --- a/crates/skate-game/src/physics/grind.rs +++ b/crates/skate-game/src/physics/grind.rs @@ -37,6 +37,7 @@ pub(crate) struct Runtime { test_depth_epsilon: f32, test_depth: f32, exiting: bool, + release: grind_forces::Release, surface_kind: u32, surface_normal: V, surface_side: V, @@ -80,6 +81,7 @@ impl Runtime { test_depth_epsilon: data.float("physics_grinds", "default", "TestDepthEpsilon")?, test_depth: data.float("physics_grinds", "default", "TestDepth")?, exiting: false, + release: grind_forces::Release::default(), surface_kind: 0, surface_normal: [0.0, 1.0, 0.0, 0.0], surface_side: [0.0; 4], @@ -492,6 +494,7 @@ pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Re | PhysicalStateId::GrindDarkslide ); physics.grind.exiting = false; + physics.grind.release = grind_forces::Release::default(); physics.grind.distance = 0.0; physics.grind.previous_position = super::solve::deck_frame(&physics.board)[3]; skater.ground_lifecycle.skeleton_elapsed_16505 = true; @@ -530,6 +533,20 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R physics.grind.launched = false; let board = super::solve::deck_frame(&physics.board); let p = &skater.player_input.processed; + if physics.grind.active { + //82D40D90; Boardslide82D41788 only exposes this exit on kind2. + let explicit = p.flags_2476 & 0x2000_0000 != 0 + && (!matches!(physics.grind.kind, 1 | 5) || physics.grind.surface_kind == 2); + let was_leaving = physics.grind.release.leaving; + physics.grind.release.begin_update(p.scalar_2652, explicit); + if !was_leaving && physics.grind.release.leaving { + bevy::log::info!("GRIND_RELEASE kind={} speed={} explicit={}", physics.grind.kind, p.scalar_2652, explicit); + } + if physics.grind.release.finish_update() { + //82D3F638: byte37/value124, using the retained request owner. + skater.wipeout.state.request(17, 0.0); + } + } if physics.grind.active && p.flags_2468 & 0x00400000 != 0 { let c = physics .grind @@ -604,6 +621,31 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R grind_contact::upright_normal(c.direction) }; physics.grind.crouch = dot3(board[2], normal).abs() * 0.4; + if physics.grind.release.leaving { + let surface = physics.grind.surface_kind; + let (strength, limit, lift, force_across) = match physics.grind.kind { + //50-50 exit82D41DF0, 5-O exit82D428C0. + 0 => if surface < 2 { (100.0, 1.0, 125.0, false) } else { (10.0, 1.0, 0.0, false) }, + 3 => if surface < 2 { (120.0, 0.9, 125.0, false) } else { (10.0, 0.9, 0.0, false) }, + //Static rail Tipslide82D420A0 and Backslash82D42A80. + 2 => (100.0, 1.5, 0.0, true), + 4 => (100.0, 0.75, if surface == 1 { 125.0 } else { 0.0 }, surface == 1), + //Boardslide82D41250 enters the existing runout/wipeout path + //on rail/ledge engagement; kind2 uses a physical side force. + _ => (90.0, 0.75, 0.0, false), + }; + if matches!(physics.grind.kind, 1 | 5) && surface != 2 { + skater.wipeout.state.request(10, 0.0); + } else { + append(physics, grind_forces::release_force( + board[3], c.centre, c.direction, normal, velocity, surface, + physics.grind.surface_side, force_across, strength, limit, lift, + ))?; + } + physics.riding.update_grind_reckoning( + &mut skater.air_reckoning.state, normal, board[2], p.flags_2468, 0.9); + return super::input_phase::update_ground(physics, skater); + } let across = cross(c.direction, normal); physics.grind.control.update( physics.grind.kind, diff --git a/docs/grind-release.md b/docs/grind-release.md new file mode 100644 index 00000000..5ef1aee2 --- /dev/null +++ b/docs/grind-release.md @@ -0,0 +1,31 @@ +# Grind release at low speed and rail ends + +TU3 reference: default_82000000_011B0000.bin, base82000000, +SHA256 f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4. +Static generated instruction extracts are under logs/biped-
.txt. + +Observed: PreUpdate82D3F4E8 owns a latched leaving phase. Decision82D40DA0 +enters it after more than60 state updates when Processed2652 speed is below +0.15m/s, or on the family exit condition. It dispatches exit forces instead +of the ordinary holding-force update. More than35 leaving updates requests +native reason17. The previous host never ran this phase and continued pinning +and orienting the board while stationary. + +Connected Release state and the existing physical body/force/request owners. +50-50 exit82D41DF0 uses100 side/125 lift below investigation kind2, otherwise +10 side/0 lift, capped by outward speed1.0. Helper82D3F850 uses the actual +contact side and support normal. Other static-rail families use their native +exit calls82D428C0/82D420A0/82D42A80/82D41250. Ordinary friction/pinning and +orientation updates do not run during release. GRIND_RELEASE records the +transition during user gameplay. + +Endpoint queries already clip segment intersections to[0,1]; losing the last +contact makes the existing selector enter Nonspecific701 and disable the +hook, then choose airborne/ground from actual contacts. No infinite extension +or endpoint teleport was added. The release phase addresses a board that +stalls while its last truck/tip remains supported at an endpoint. + +Scope: common slow-speed/explicit release and static-rail exit forces. +Specialized tipslide drop-in and investigation-driven early-exit branches +remain separate existing gaps; this does not claim full retail parity. +Build only, no tests or automated gameplay. Visual checking is left to user.