From 55cc1eff82ba3d4f84695229d24c5ae7bb237312 Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 16:20:06 +0100 Subject: [PATCH] Implement TU3 no-comply, footplant and handplant physics --- LaunchPlants.bat | 14 + crates/skate-core/src/air/reckoning/mod.rs | 20 ++ .../skate-core/src/input/trick_intentions.rs | 18 ++ .../src/physics/skeleton_air_frames.rs | 18 ++ .../src/player/input_phase/air_output.rs | 9 + .../src/player/selector/offboard.rs | 4 +- .../skate-core/src/player/wipeout/ground.rs | 13 + crates/skate-core/src/player/wipeout/mod.rs | 2 +- .../src/graph_host/action_conditions.rs | 2 +- .../graph_host/motion_gameplay_conditions.rs | 32 +++ .../src/graph_host/motion_native.rs | 1 + .../skate-game/src/graph_host/motion_nodes.rs | 3 + .../src/graph_host/motion_tricks.rs | 36 +++ crates/skate-game/src/physics.rs | 3 + .../skate-game/src/physics/air_reckoning.rs | 4 + .../skate-game/src/physics/animation_phase.rs | 5 + crates/skate-game/src/physics/boneless.rs | 103 +++++++ crates/skate-game/src/physics/footplant.rs | 20 +- .../src/physics/footplant/ground.rs | 180 ++++++++++++ .../skate-game/src/physics/footplant/math.rs | 29 +- .../src/physics/footplant/settings.rs | 22 ++ .../src/physics/footplant/update.rs | 50 +++- crates/skate-game/src/physics/frame.rs | 4 + crates/skate-game/src/physics/ground_phase.rs | 1 + crates/skate-game/src/physics/handplant.rs | 271 ++++++++++++++++++ .../src/physics/handplant/contact.rs | 242 ++++++++++++++++ crates/skate-game/src/physics/handplant/ik.rs | 90 ++++++ .../src/physics/handplant/rotation.rs | 50 ++++ .../src/physics/handplant/settings.rs | 96 +++++++ .../src/physics/handplant/trajectory.rs | 269 +++++++++++++++++ crates/skate-game/src/physics/input_phase.rs | 4 + .../skate-game/src/physics/input_teleport.rs | 2 + .../src/physics/known_air/runtime.rs | 2 + .../skate-game/src/physics/plant_skeleton.rs | 91 ++++++ .../src/physics/player_state/pre_state.rs | 3 +- .../src/physics/player_state/publication.rs | 11 +- .../src/physics/player_state/transition.rs | 12 +- crates/skate-game/src/physics/skater.rs | 4 + crates/skate-game/src/physics/wipeout.rs | 7 + docs/plants.md | 110 +++++++ 40 files changed, 1829 insertions(+), 28 deletions(-) create mode 100644 LaunchPlants.bat create mode 100644 crates/skate-game/src/physics/boneless.rs create mode 100644 crates/skate-game/src/physics/footplant/ground.rs create mode 100644 crates/skate-game/src/physics/handplant.rs create mode 100644 crates/skate-game/src/physics/handplant/contact.rs create mode 100644 crates/skate-game/src/physics/handplant/ik.rs create mode 100644 crates/skate-game/src/physics/handplant/rotation.rs create mode 100644 crates/skate-game/src/physics/handplant/settings.rs create mode 100644 crates/skate-game/src/physics/handplant/trajectory.rs create mode 100644 crates/skate-game/src/physics/plant_skeleton.rs create mode 100644 docs/plants.md diff --git a/LaunchPlants.bat b/LaunchPlants.bat new file mode 100644 index 00000000..c0c4a206 --- /dev/null +++ b/LaunchPlants.bat @@ -0,0 +1,14 @@ +@echo off +setlocal +cd /d "%~dp0" +set "PLANT_EXE=%~dp0bin\plants\skate3rust.exe" +set "PLANT_ASSETS=C:\Users\Daddy\AppData\Local\Skate3RustEngine\installations\957f4f78db9b4f59959f10dff8ff6a84\assets" +if not exist "%PLANT_EXE%" ( + echo Missing task build: "%PLANT_EXE%" + pause + exit /b 1 +) +"%PLANT_EXE%" --assets "%PLANT_ASSETS%" --test-world %* +set "PLANT_EXIT=%ERRORLEVEL%" +if not "%PLANT_EXIT%"=="0" pause +exit /b %PLANT_EXIT% diff --git a/crates/skate-core/src/air/reckoning/mod.rs b/crates/skate-core/src/air/reckoning/mod.rs index e5d138fc..83ed5b00 100644 --- a/crates/skate-core/src/air/reckoning/mod.rs +++ b/crates/skate-core/src/air/reckoning/mod.rs @@ -125,5 +125,25 @@ fn vector(v: [f32; 4]) -> Vector3 { Vector3::new(v[0], v[1], v[2]) } +///Handplant Reckoning82D8E1C0. Uses the ordinary persistent normal filters. +pub fn update_plant(orientation:&mut GroundOrientation,frames:&mut ReckoningFrames, + body_spin:&mut BodySpinState,state:&mut AirState,settings:&Settings, + up:[f32;4],heading:[f32;4],reverse:bool) { + frames.heading=heading; + body_spin::update(body_spin,&settings.body_spin,0.0,0.0,true,0); + let previous=lanes(orientation.up); + orientation.dynamic_up=orientation.up; + frames.target_lean_angle=0.0;state.secondary_lean_angle=0.0; + let normal=normalize(orientation.ground_filter.update(settings.ground_normal_smoothing,up)); + orientation.ground_normal=vector(normal); + let up=normalize_safe(limit_angle(normal,previous,1.0),previous); + orientation.up_velocity=vector(std::array::from_fn(|i|up[i]-previous[i])); + orientation.up=vector(up);orientation.target=orientation.up; + orientation.slow_filter.filter(up);orientation.fast_filter.filter(up); + orientation.slow_filter.publish_current(up);orientation.fast_filter.publish_current(up); + frames.calculate_transform(up,normal); + frames.calculate_tilt(reverse,&settings.tilt_vs_rotation,&settings.tilt_vs_slope); +} + #[cfg(test)] mod tests; diff --git a/crates/skate-core/src/input/trick_intentions.rs b/crates/skate-core/src/input/trick_intentions.rs index 8027576d..5894a734 100644 --- a/crates/skate-core/src/input/trick_intentions.rs +++ b/crates/skate-core/src/input/trick_intentions.rs @@ -81,6 +81,24 @@ pub fn produce(controller: &DerivedControllerInput) -> Vec { if right_y != 0.0 { emit("TweakY", right_y); } + //Fill8259A554/8259AF60: current packed bit28 (RB), without an actor gate. + //The stock ground graph attaches GrabWorld; physics selects valid coping. + if current_flags & (1 << 28) != 0 { + emit("GrabWorld", 1.0); + } + //Fill calls8259BB18 with the CURRENT RawControllerInput (words7..13). + //Descriptor constructors82F84F28..82F84F98 establish these five names. + if right_x != 0.0 { + emit("HandPlantTweakX", right_x); + } + if right_y != 0.0 { + emit("HandPlantTweakY", right_y); + } + for (bit, name) in [(20, "HandPlantDismount"), (21, "HandPlantOneFootRight"), (23, "HandPlantOneFootLeft")] { + if current_flags & (1 << bit) != 0 { + emit(name, 1.0); + } + } output } diff --git a/crates/skate-core/src/physics/skeleton_air_frames.rs b/crates/skate-core/src/physics/skeleton_air_frames.rs index a422dac1..823e54a9 100644 --- a/crates/skate-core/src/physics/skeleton_air_frames.rs +++ b/crates/skate-core/src/physics/skeleton_air_frames.rs @@ -103,3 +103,21 @@ pub fn finish_known_air( roots.predicted_board_position = effective_board[3]; roots.supplied_prediction = Some(effective_board[3]); } + +///82BDEEB8/82BE0E80: anchor an animation bone or COM in world space. +///Plants retain heading_alignment and do not consume board prediction. +pub fn update_plant_roots( + roots: &mut SkeletonRootFrames, + reckoning: &Transform, + world_anchor: [f32; 4], + animation_anchor: [f32; 4], +) { + let mut world = compose_affine(reckoning, &roots.heading_alignment); + world[3] = std::array::from_fn(|i| { + let x = world[0][i] * animation_anchor[0]; + let y = world[1][i].mul_add(animation_anchor[1], x); + world_anchor[i] - world[2][i].mul_add(animation_anchor[2], y) + }); + roots.animation_to_world = orthonormalize(world); + roots.world_to_animation = inverse_rigid(&roots.animation_to_world); +} diff --git a/crates/skate-core/src/player/input_phase/air_output.rs b/crates/skate-core/src/player/input_phase/air_output.rs index 0dc33db3..89775ea8 100644 --- a/crates/skate-core/src/player/input_phase/air_output.rs +++ b/crates/skate-core/src/player/input_phase/air_output.rs @@ -24,10 +24,19 @@ pub struct AirOutputFields { pub flag_444: u8, /// Air324 high bits used by IsHandPlanting82BA55A8. pub handplant_flags_324: u32, + pub handplant_position_304: RawVector, + pub handplant_time_320: f32, pub vector_160: RawVector, pub scalar_184: f32, pub landing_normal_144: RawVector, pub jump_height_200: f32, + /// FootPlantManager::Fill82D71040: predicted contact and ground-plant duration. + pub footplant_contact_time_208: f32, + pub footplant_duration_212: f32, + pub footplant_surface_height_216: f32, + pub footplant_surface_224: u32, + pub footplant_left_449: u8, + pub footplant_right_450: u8, pub reached_apex_436: u8, pub flag_441: u8, pub flag_446: u8, diff --git a/crates/skate-core/src/player/selector/offboard.rs b/crates/skate-core/src/player/selector/offboard.rs index 363e4767..e2acb71a 100644 --- a/crates/skate-core/src/player/selector/offboard.rs +++ b/crates/skate-core/src/player/selector/offboard.rs @@ -85,7 +85,9 @@ impl StateSelector { PhysicalStateId::FootPlant => { if facts.wipeout { PhysicalStateId::WipeoutGround - } else if !p.has_2480(1) { + //82D8B970 loads the first byte of the big-endian word, + //then tests its low bit: full-word bit24 is Air448. + } else if !p.has_2480(0x0100_0000) { p.air_variant_from_2468() } else { current diff --git a/crates/skate-core/src/player/wipeout/ground.rs b/crates/skate-core/src/player/wipeout/ground.rs index c762b1ed..ee3bf1a1 100644 --- a/crates/skate-core/src/player/wipeout/ground.rs +++ b/crates/skate-core/src/player/wipeout/ground.rs @@ -126,3 +126,16 @@ pub fn check_animation(state: &mut Requests, s: &Settings, mode: &Mode, f: &Fram state.request(20, 0.0); } } + +///82D8FDC0, shared FootPlant/HandPlant postphysics collision checks. +pub fn check_plant(state: &mut Requests, s: &Settings, f: &Frame) { + state.mode = 3; + if f.maximum_pose_error > s.ground.max_squash { + state.request(18, 0.0); + } else if super::common::length(f.pose_error) > s.air.max_displacement { + state.request(1, 0.0); + } else if super::common::force(f, s.air.max_contact, s.air.max_arm_contact) { + state.request(0, 0.0); + } + common::closing(state, f, s.air.xz_trick, s.air.y_trick); +} diff --git a/crates/skate-core/src/player/wipeout/mod.rs b/crates/skate-core/src/player/wipeout/mod.rs index 34718157..1a26f369 100644 --- a/crates/skate-core/src/player/wipeout/mod.rs +++ b/crates/skate-core/src/player/wipeout/mod.rs @@ -7,7 +7,7 @@ mod ground; mod requests; pub use air::check as check_air; pub use data::{AirSettings, Frame, GroundSettings, Mode, RequestInput, Settings, V}; -pub use ground::{check as check_ground, check_animation as check_ground_animation}; +pub use ground::{check as check_ground, check_animation as check_ground_animation, check_plant}; pub use requests::Requests; pub use common::force as regional_force; diff --git a/crates/skate-game/src/graph_host/action_conditions.rs b/crates/skate-game/src/graph_host/action_conditions.rs index 73d85cb9..8e401980 100644 --- a/crates/skate-game/src/graph_host/action_conditions.rs +++ b/crates/skate-game/src/graph_host/action_conditions.rs @@ -39,7 +39,7 @@ impl ActionCondition { target: None, numeric: super::condition_nodes::numeric(a), }, - "IsGrabbingObject" | "IsHandPlanting" => Self::Physical(GameplayCondition::parse(a)?), + "IsGrabbingObject" | "IsHandPlanting" | "IsFootPlanting" => Self::Physical(GameplayCondition::parse(a)?), "IsTricking" => Self::Tricking, "IsInLocomotion" => Self::InLocomotion, "DisableDismount" => Self::DisableDismount, diff --git a/crates/skate-game/src/graph_host/motion_gameplay_conditions.rs b/crates/skate-game/src/graph_host/motion_gameplay_conditions.rs index 410162e1..d85a2272 100644 --- a/crates/skate-game/src/graph_host/motion_gameplay_conditions.rs +++ b/crates/skate-game/src/graph_host/motion_gameplay_conditions.rs @@ -19,6 +19,12 @@ pub struct GameplayConditions { pub can_land_on_board: bool, pub hippy_hurdling: bool, pub handplant_flags: u32, + pub handplant_time: f32, + /// anim_handplant/default layout0/4/8: out, into, antic. + pub handplant_thresholds: [f32; 3], + pub footplant_active: bool, + pub footplant_duration: f32, + pub footplant_contact_time: f32, pub time_to_skitch: f32, pub skitch_transition_time: f32, /// TimeToLand82BA7250 reads Air184 when Air437 is valid. @@ -50,6 +56,10 @@ pub enum GameplayCondition { DarkCatchRequested, OkToDoTrickOnStairs, HandPlanting { state: u8, direction: u8 }, + FootPlanting, + PrepareFootplant, + NewHandplantPosition, + PlayHandplant { phase: usize }, EnteringSkitch, Skitching, } @@ -75,6 +85,10 @@ impl GameplayCondition { | "IsDarkCatchRequested" | "OkToDoTrickOnStairs" | "IsHandPlanting" + | "IsFootPlanting" + | "ShouldPrepareOneFootAirForFootplant" + | "HasNewHandPlantPos" + | "ShouldPlayHandPlantAnim" | "IsEnteringSkitch" | "IsSkitching" ) @@ -102,6 +116,13 @@ impl GameplayCondition { "OkToDoTrickOnStairs" => Self::OkToDoTrickOnStairs, "IsEnteringSkitch" => Self::EnteringSkitch, "IsSkitching" => Self::Skitching, + "IsFootPlanting" => Self::FootPlanting, + "ShouldPrepareOneFootAirForFootplant" => Self::PrepareFootplant, + "HasNewHandPlantPos" => Self::NewHandplantPosition, + "ShouldPlayHandPlantAnim" => Self::PlayHandplant { phase: match a.text("anim") { + Some("antic") => 2, Some("into") => 1, Some("out") => 0, + value => return Err(format!("Unauthored handplant animation phase {value:?}")), + } }, "IsHandPlanting" => Self::HandPlanting { //82BA54A0 compares these authored strings in this order. state: match a.text("state").unwrap_or("") { @@ -155,6 +176,17 @@ impl GameplayCondition { _ => false, } } + // Factory82BCB308 -> VT82321070 slot48=82BBD888. + // Both graphs read Air448 and Air212, including the entry frame. + Self::FootPlanting => p.footplant_active && p.footplant_duration >= 0.0, + //82BBD7F0, literals820641A8 and8209975C. + Self::PrepareFootplant => p.footplant_contact_time >= 0.1 && p.footplant_contact_time <= 0.5, + Self::NewHandplantPosition => p.handplant_flags & 0x4000_0000 != 0, //82BBDB98 + Self::PlayHandplant { phase } => { + //82BBDA98 subtracts one simulation tick before the comparison. + let threshold = p.handplant_thresholds[*phase]; + p.handplant_time - f32::from_bits(0x3c888889) < if *phase == 0 { -threshold } else { threshold } + } Self::DroppingBoard | Self::Dark | Self::UnderflipRequested diff --git a/crates/skate-game/src/graph_host/motion_native.rs b/crates/skate-game/src/graph_host/motion_native.rs index 0040cdb0..bb6ec285 100644 --- a/crates/skate-game/src/graph_host/motion_native.rs +++ b/crates/skate-game/src/graph_host/motion_native.rs @@ -79,6 +79,7 @@ impl Operation { /// this game.82595D08 sets a name and ORs a bit in the graph's score packet. #[derive(Default)] pub struct ScorePacket { + pub handplant: Option<(skate_core::animation::output::attributes::AttributeName, [f32; 2])>, pub grab: Option<(skate_core::animation::output::attributes::AttributeName, [f32; 2])>, pub trick: Option, pub name: Option, diff --git a/crates/skate-game/src/graph_host/motion_nodes.rs b/crates/skate-game/src/graph_host/motion_nodes.rs index 614c72a0..5543ab89 100644 --- a/crates/skate-game/src/graph_host/motion_nodes.rs +++ b/crates/skate-game/src/graph_host/motion_nodes.rs @@ -149,6 +149,9 @@ impl OperationFactory for MotionFactory { )), "ClearTrickAttr" => Some(MotionOperation::ClearTrickAttr), "SetTrickHeight" + | "FootPlantAbsorb" + | "SetHandPlantAnticLength" + | "ScoringHandPlants" | "SetTrickAttr" | "ScoringTrick" | "MonitorUnderflip" diff --git a/crates/skate-game/src/graph_host/motion_tricks.rs b/crates/skate-game/src/graph_host/motion_tricks.rs index c986f716..a0d7b128 100644 --- a/crates/skate-game/src/graph_host/motion_tricks.rs +++ b/crates/skate-game/src/graph_host/motion_tricks.rs @@ -10,6 +10,9 @@ use skate_data::state_graph::attributes::Attributes; #[derive(Clone, Debug, PartialEq)] pub enum Operation { + FootPlantAbsorb, + HandplantAntic, + HandplantScore { base: String, directions: [Option;4], intents: [String;2] }, Height { from: AttributeName, rename: AttributeName, @@ -26,6 +29,13 @@ pub enum Operation { impl Operation { pub fn parse(a: &Attributes<'_>) -> Option { Some(match a.text("name")? { + "FootPlantAbsorb" => Self::FootPlantAbsorb, + "SetHandPlantAnticLength" => Self::HandplantAntic, + "ScoringHandPlants" => Self::HandplantScore { + base:a.text("handplantname").unwrap_or("").to_owned(), + directions:["up","left","down","right"].map(|n|a.text(n).map(str::to_owned)), + intents:["intentX","intentY"].map(|n|a.text(n).unwrap_or("").to_owned()), + }, "SetTrickHeight" => Self::Height { from: encode(a.text("from").unwrap_or("").as_bytes()), rename: encode(a.text("rename").unwrap_or("").as_bytes()), @@ -56,6 +66,32 @@ pub fn execute( phase: u8, ) -> Result<(), String> { match operation { + Operation::HandplantAntic if phase==0 => { + //VT82321360 Begin=82BBE5D0; shared clip lengths from82B959E8. + let duration=|name|->Result { + let clip=host.animation.metadata().clip(name)?; + Ok((f32::from_bits(clip.frames_bits)-1.0)/(f32::from_bits(clip.base_speed_bits)*f32::from_bits(clip.fps_bits))) + }; + let normal=duration("INVERT_HANDPLANT_FS_0_ANTIC")?; + let late=duration("INVERT_HANDPLANT_FS_0_ANTIC_LATE")?; + let p=host.gameplay_conditions.as_ref().ok_or("Handplant antic requires physical output")?; + let value=((normal-(p.handplant_thresholds[2]-p.handplant_time)-late)/(normal-late)).clamp(0.0,1.0); + host.animation.set_attribute(SettableAttribute {name:encode(b"antic_length"),value,normalized:false,sequence_id:-1}); + } + Operation::HandplantScore {base,directions,intents} if phase==1 => { + //82BBF758 uses the same angular windows as ScoringGrabs82BBEF60. + let xy=intents.each_ref().map(|n|host.animation.filtered_intent(n).unwrap_or(0.0)); + let name=super::motion_stock_gameplay::select_grab_score(&base,directions.each_ref().map(|s|s.as_deref()),xy[0],xy[1]); + host.score_packet.handplant=Some((encode(name.as_bytes()),xy)); + } + Operation::FootPlantAbsorb if phase == 1 => { + //82BBE4E0 reads the manager's curve duration, Air212. + let value = host.gameplay_conditions.as_ref() + .ok_or("FootPlantAbsorb requires physical output")?.footplant_duration; + host.animation.set_attribute(SettableAttribute { + name: encode(b"absorblength"), value, normalized: false, sequence_id: -1, + }); + } Operation::Height { from, rename, diff --git a/crates/skate-game/src/physics.rs b/crates/skate-game/src/physics.rs index 38f053cf..5c5f837c 100644 --- a/crates/skate-game/src/physics.rs +++ b/crates/skate-game/src/physics.rs @@ -10,6 +10,9 @@ mod colliders; mod controls; mod foot_ik; mod footplant; +mod plant_skeleton; +mod boneless; +mod handplant; mod foot_ik_queries; mod foot_physical_output; pub(crate) mod ground; diff --git a/crates/skate-game/src/physics/air_reckoning.rs b/crates/skate-game/src/physics/air_reckoning.rs index 5ed417b2..c8c6bd9e 100644 --- a/crates/skate-game/src/physics/air_reckoning.rs +++ b/crates/skate-game/src/physics/air_reckoning.rs @@ -16,6 +16,10 @@ pub(crate) struct AirReckoning { modes: [settings::Mode; 5], } impl AirReckoning { + pub fn update_plant(&mut self,riding:&mut RidingOutputs,p:&ProcessedPhysicsInput,up:[f32;4],heading:[f32;4]) { + reckoning::update_plant(&mut riding.reckoning,&mut riding.reckoning_frames, + &mut riding.body_spin,&mut self.state,&self.settings,up,heading,p.flags_2468&(1<<20)!=0); + } pub fn load(data: &Collections) -> Result { let (settings, modes) = settings::load(data)?; Ok(Self { diff --git a/crates/skate-game/src/physics/animation_phase.rs b/crates/skate-game/src/physics/animation_phase.rs index 5e794057..c265ed3b 100644 --- a/crates/skate-game/src/physics/animation_phase.rs +++ b/crates/skate-game/src/physics/animation_phase.rs @@ -99,6 +99,11 @@ pub(crate) fn advance( can_land_on_board: physical.off_board.flag_316 != 0, hippy_hurdling: physical.off_board.hippy_hurdling_317 != 0, handplant_flags: physical.air.handplant_flags_324, + handplant_time: physical.air.handplant_time_320, + handplant_thresholds: skater.handplant.animation_thresholds(), + footplant_active: physical.air.flag_448 != 0, + footplant_duration: physical.air.footplant_duration_212, + footplant_contact_time: physical.air.footplant_contact_time_208, time_to_skitch: physical.ground.scalar_276, skitch_transition_time: profile.skitch_transition_time, time_to_land: physical.air.scalar_184, diff --git a/crates/skate-game/src/physics/boneless.rs b/crates/skate-game/src/physics/boneless.rs new file mode 100644 index 00000000..c73deb03 --- /dev/null +++ b/crates/skate-game/src/physics/boneless.rs @@ -0,0 +1,103 @@ +//! Physical state602, TU3 vtable82327434. Stock no-comply/boneless/fastplant +//! animations select this state with AnimSkateboard and launch with FootJump. +use super::footplant::math::*; +use super::{GamePhysics, SkaterRuntime, air_phase, plant_skeleton}; +use skate_core::point_graph::PointGraph; +use skate_data::collections::Collections; + +pub(crate) struct Boneless { + pub toe: usize, //52 + pub anchor: [f32; 4], //64 + pub right: bool, //80 + curves: [PointGraph<8>; 4], +} +impl Boneless { + pub fn load(data: &Collections) -> Result { + let graph = |name| -> Result<_, String> { + let words = data + .words::<20>("Hash_CCB95A83C78B4FF9", "default", name)? + .map(f32::from_bits); + Ok(PointGraph { + x: words[4..12].try_into().unwrap(), + y: words[12..20].try_into().unwrap(), + }) + }; + Ok(Self { + toe: 15, + anchor: [0.0; 4], + right: false, + curves: [ + graph("Hash_6D781EFAF01E707D")?, + graph("Hash_C9112CCD0BCB1850")?, + graph("Hash_88D0CDEFA38A36D6")?, + graph("Hash_9228B7F18C223F9C")?, + ], + }) + } +} + +///82D4C900: anchor the last completed physical toe position. +pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + skater.ground_lifecycle.board_animated_290 = 1; + let drive = &mut physics.board.hook_mut().drive; + //Globals12/912 at822F860C/822F8990; linear HardDrive, angular SoftDrive. + drive.dynamics[..4].copy_from_slice(&[0x426f_ffff, 0, 0x4560_fffe, 2]); + drive.enable_angular_soft(); + let right = skater.player_input.processed.flags_2468 & (1 << 26) != 0; + skater.boneless.toe = if right { 19 } else { 15 }; + skater.boneless.anchor = skater.skeleton.record.pose[skater.boneless.toe][3]; + skater.boneless.right = right; + Ok(()) +} + +///82D4C9B8. Authored FootJump controls every launch request; no host timer. +pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + plant_skeleton::advance( + physics, + skater, + skater.boneless.anchor, + Some(skater.boneless.toe), + )?; + plant_skeleton::hold_foot(skater, skater.boneless.right, skater.boneless.anchor, 3); + if skater.player_input.processed.flags_2480 & (1 << 13) != 0 { + launch(physics, skater)?; + } + Ok(()) +} + +///82D4CB18/82D4CC18, four PointNegGraphData8 values from the stock schema. +fn launch(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + let p = &skater.player_input.processed; + let up = p.vectors_544_560_592_608[0].map(f32::from_bits); + let current = p.vectors_544_560_592_608[3].map(f32::from_bits); + let prepared = p.prepared_jump_704.map(f32::from_bits); + let horizontal = sub(prepared, scale(up, dot(prepared, up))); + let speed = length(horizontal); + let g = &skater.boneless.curves; + let vertical = g[0].evaluate(up[1]) * g[1].evaluate(speed); + let scaled_speed = g[2].evaluate(speed) * speed; + let a = madd(up, vertical - dot(current, up), current); + let b = madd(normalize(horizontal), scaled_speed, scale(up, vertical)); + let blend = g[3].evaluate(up[1]); + let mut velocity = madd(b, blend, scale(a, 1.0 - blend)); + if (length(velocity) - length(current)).abs() > 10.0 { + velocity = current; + } + let forward = p.effective_anim_transform_192[2].map(f32::from_bits); //Processed224 + let component = dot(velocity, forward).max(0.0); + if component < 1.92 { + velocity = madd(forward, 1.92 - component, velocity); + } + let com = p.vectors_544_560_592_608[2].map(f32::from_bits); + let position = madd(up, dot(sub(skater.boneless.anchor, com), up) + 0.25, com); + let mut info = air_phase::launch_info(physics, skater)?; + info.start_velocity = velocity; + info.start_position_override = position; + info.board_position_override = position; + info.use_position_override = true; + info.player_jumped = true; + let input = air_phase::selector_input(physics, skater)?; + skater.trajectory.launch(info, input, &physics.world)?; + skater.trajectory.update(input, &physics.world)?; + Ok(()) +} diff --git a/crates/skate-game/src/physics/footplant.rs b/crates/skate-game/src/physics/footplant.rs index 9645c505..c8d94cea 100644 --- a/crates/skate-game/src/physics/footplant.rs +++ b/crates/skate-game/src/physics/footplant.rs @@ -2,10 +2,11 @@ //! Query results are retained separately from pending query ownership. use skate_core::air::trajectory::{QueryResult, Trajectory}; use skate_data::collections::Collections; -mod math; +pub(super) mod math; mod pose; mod settings; mod update; +pub(super) mod ground; use settings::Settings; pub(crate) use update::FootplantFrame; pub(crate) type V = [f32; 4]; @@ -36,11 +37,13 @@ pub(crate) struct Footplant { pub(super) selected_record: V, //272 pub(super) leg_direction: V, //336 pub(super) vectors_352_368: [V; 2], + pub(super) curve: [V; 4], //448/464/480/496 pub(super) physical_com: V, //384 pub(super) animation_com: V, //400 pub(super) launch_direction: V, //416 pub(super) locked_target: V, //432 pub(super) request: Trajectory, //528..591 + pub(super) completed_trajectory: Trajectory, //query144, retained with result } impl Footplant { pub fn load(data: &Collections) -> Result { @@ -73,6 +76,7 @@ impl Footplant { selected_record: [0.0; 4], leg_direction: [0.0; 4], vectors_352_368: [[0.0; 4]; 2], + curve: [[0.0; 4]; 4], physical_com: [0.0; 4], animation_com: [0.0; 4], launch_direction: [0.0; 4], @@ -83,6 +87,7 @@ impl Footplant { acceleration: [0.0; 4], duration: -1.0, }, + completed_trajectory: Trajectory { position: [0.0;4], velocity: [0.0;4], acceleration: [0.0;4], duration: -1.0 }, }; state.reset(); Ok(state) @@ -123,4 +128,17 @@ impl Footplant { self.enabled = false; self.reset(); } + /// Common ProcessOutput82DB71B0 calls82D71040 before selected state Fill. + pub fn publish(&self, out: &mut skate_core::player::input_phase::AirOutputFields) { + if self.hit { + out.footplant_contact_time_208 = self.contact_time; + out.footplant_duration_212 = self.scalar_596; + out.flag_447 = u8::from(self.perform); + out.flag_448 = u8::from(self.perform || self.flag_627); + out.footplant_surface_224 = self.surface; + out.footplant_left_449 = u8::from(self.flag_627 && self.selected_toe == Some(15)); + out.footplant_right_450 = u8::from(self.flag_627 && self.selected_toe == Some(19)); + out.footplant_surface_height_216 = self.contact[1]; + } + } } diff --git a/crates/skate-game/src/physics/footplant/ground.rs b/crates/skate-game/src/physics/footplant/ground.rs new file mode 100644 index 00000000..73dcedb5 --- /dev/null +++ b/crates/skate-game/src/physics/footplant/ground.rs @@ -0,0 +1,180 @@ +//! FootPlant601, vtable82327400 and manager82D704B0/82D70E40. +use super::{Footplant, math::*}; +use crate::physics::{GamePhysics, SkaterRuntime, air_phase, plant_skeleton}; + +pub(in crate::physics) fn enter( + physics: &mut GamePhysics, + skater: &mut SkaterRuntime, +) -> Result<(), String> { + physics + .board + .hook_mut() + .drive + .enable_angular_only(&mut skater.ground_lifecycle.board_animated_290); + //82D91330 sets the foot/toe volume group and clears their disable counters. + for part in [15, 16, 19, 20] { + skater.skeleton_collision.parts[part].volume_group = 4; + skater.skeleton_collision.parts[part].enabled = false; + skater.skeleton_collision.disable_count[part] = 0; + } + let p = &skater.player_input.processed; + let frames = skater.footplant.start( + p.vectors_544_560_592_608[2].map(f32::from_bits), + p.vectors_544_560_592_608[3].map(f32::from_bits), + ); + if frames != 0 { + //The native Start only disables collisions; the first Update sets IK. + let right = skater.footplant.selected_toe == Some(19); + for part in if right { [19, 20, 21] } else { [15, 16, 17] } { + skater.skeleton_collision.pending_reenable = true; + skater.skeleton_collision.disable_count[part] = frames; + skater.skeleton_collision.parts[part].enabled = false; + } + } + Ok(()) +} + +impl Footplant { + fn start(&mut self, com: [f32; 4], velocity: [f32; 4]) -> u32 { + self.perform = false; + self.scalar_600 = 0.0; + self.flag_627 = true; + let contact = self.adjusted_contact; + self.vectors_352_368[1] = contact; + let offset = sub(com, contact); + let radial = normalize(offset); + let multiplier = self + .settings + .radial_speed_scale + .evaluate(length(scale(radial, dot(velocity, radial)))); + let rotation = cross(offset, velocity); + let angular_speed = length(rotation); + //830BD300 is initialized by82F82690 from821647E0. + if !(angular_speed > f32::from_bits(0x3780_0000)) { + return 0; + } + let axis = scale(rotation, reciprocal(angular_speed)); + let angular_rate = angular_speed * multiplier; + //8286CD88 projects both vectors into the plane;8296EC98 signs acos. + let normal = scale(axis, -1.0); + let up = [0.0, 1.0, 0.0, 0.0]; + let projected_up = normalize(sub(up, scale(normal, dot(up, normal)))); + let mut angle = if length(projected_up) == 0.0 { + 0.0 + } else { + skate_core::trigonometry::acos(dot(radial, projected_up).clamp(-1.0, 1.0)) + }; + if dot(cross(radial, projected_up), normal) < 0.0 { + angle = -angle; + } + let min_end = angle + angular_rate * self.settings.min_duration; + let max_end = angle + angular_rate * self.settings.max_duration; + let end_angle = min_end + .max(self.settings.end_angle * f32::from_bits(0x3c8e_fa35)) + .min(max_end); + let delta = end_angle - angle; + self.scalar_596 = delta / angular_rate; + //Quaternion(axis,delta) applied in the native two-cross-product order. + let (sin, cos) = skate_core::trigonometry::sin_cos(delta * 0.5); + let q = scale(axis, sin); + let end_radial = madd(cross(q, madd(radial, cos, cross(q, radial))), 2.0, radial); + let end = madd(end_radial, self.settings.end_leg_length, contact); + let middle = normalize(add(end_radial, radial)); + let release = madd( + sub(velocity, scale(middle, dot(velocity, middle))), + multiplier, + scale(middle, self.settings.release_outward_speed), + ); + self.scalar_612 = length(release); + self.vectors_352_368[0] = scale(release, reciprocal(self.scalar_612)); + self.curve = [ + com, + madd( + scale(velocity, multiplier), + self.scalar_596 * self.settings.start_handle, + com, + ), + sub( + end, + scale(release, self.scalar_596 * self.settings.end_handle), + ), + end, + ]; + (4 - (self.scalar_596 * f32::from_bits(0xc26f_ffff)) as i32) as u32 + } + + ///82D70CE0: authored BodyAdjustX/Z move the release point, retaining + ///the last handle length.82C1E170 removes only positive up projection. + fn adjust(&mut self, t: f32, input: [f32; 2]) { + let amount = (1.0 - t).mul_add(0.02, t * 0.01); + let mut movement = [input[0] * amount, 0.0, input[1] * amount, 0.0]; + let projection = dot(movement, normalize(self.current_up)); + if projection > 0.0 { + movement = sub(movement, scale(self.current_up, projection)); + } + let previous = self.curve[3]; + self.curve[3] = add(previous, movement); + let direction = normalize(sub(self.curve[3], self.curve[2])); + self.curve[2] = sub( + self.curve[3], + scale(direction, length(sub(self.curve[2], previous))), + ); + self.vectors_352_368[0] = direction; + } +} + +pub(in crate::physics) fn update( + physics: &mut GamePhysics, + skater: &mut SkaterRuntime, +) -> Result<(), String> { + let fp = &mut skater.footplant; + fp.scalar_600 += STEP; + let t = fp.scalar_600 / fp.scalar_596; + fp.adjust(t, skater.animation_input.extra.body_adjust); + let u = 1.0 - t; + let position = madd( + fp.curve[3], + t * t * t, + madd( + fp.curve[2], + 3.0 * t * t * u, + madd(fp.curve[0], u * u * u, scale(fp.curve[1], 3.0 * t * u * u)), + ), + ); + let launched = fp.scalar_600 > fp.scalar_596; + if launched { + fp.flag_627 = false; + } + let anchor = fp.vectors_352_368[1]; + let velocity = scale(fp.vectors_352_368[0], fp.scalar_612); + let right = fp.selected_toe != Some(15); + plant_skeleton::hold_foot(skater, right, anchor, 0); + plant_skeleton::advance(physics, skater, position, None)?; + if launched { + let mut info = air_phase::launch_info(physics, skater)?; + info.start_velocity = velocity; + info.start_position_override = add(anchor, [0.0, 0.2, 0.0, 0.0]); + info.board_position_override = add(info.start_position_override, [0.0, -0.1, 0.0, 0.0]); + info.use_position_override = true; + info.player_jumped = true; + let input = air_phase::selector_input(physics, skater)?; + skater.trajectory.launch(info, input, &physics.world)?; + skater.trajectory.update(input, &physics.world)?; + } + Ok(()) +} + +///82D4C6D8, after the shared plant collision check82D8FDC0. +pub(in crate::physics) fn post_physics(skater: &mut SkaterRuntime) { + let p = &skater.player_input.processed; + let down = scale(p.vectors_544_560_592_608[0].map(f32::from_bits), -1.0); + let velocity = p.vectors_544_560_592_608[3].map(f32::from_bits); + let descending = dot(velocity, down); + let horizontal = length(sub(velocity, scale(down, descending))); + if descending > skater.footplant.settings.max_descending_speed { + skater.wipeout.state.request(6, descending); + } + if horizontal > skater.footplant.settings.max_horizontal_speed { + skater.wipeout.state.request(23, 0.0); + } +} diff --git a/crates/skate-game/src/physics/footplant/math.rs b/crates/skate-game/src/physics/footplant/math.rs index 6873c422..fc9ed161 100644 --- a/crates/skate-game/src/physics/footplant/math.rs +++ b/crates/skate-game/src/physics/footplant/math.rs @@ -1,23 +1,26 @@ //! Original vector operation order from82D6FD60/82D6F5F0/82D70070. //! Estimate seeds use independent PC math; Xenon bit equality is unverified. use super::V; -pub(super) const STEP: f32 = f32::from_bits(0x3c888889); -pub(super) fn dot(a: V, b: V) -> f32 { +pub(in crate::physics) const STEP: f32 = f32::from_bits(0x3c888889); +pub(in crate::physics) fn dot(a: V, b: V) -> f32 { (a[0] * b[0] + a[1] * b[1]) + a[2] * b[2] } -pub(super) fn add(a: V, b: V) -> V { +pub(in crate::physics) fn cross(a: V, b: V) -> V { + [a[1].mul_add(b[2], -a[2]*b[1]), a[2].mul_add(b[0], -a[0]*b[2]), a[0].mul_add(b[1], -a[1]*b[0]), 0.0] +} +pub(in crate::physics) fn add(a: V, b: V) -> V { std::array::from_fn(|i| a[i] + b[i]) } -pub(super) fn sub(a: V, b: V) -> V { +pub(in crate::physics) fn sub(a: V, b: V) -> V { std::array::from_fn(|i| a[i] - b[i]) } -pub(super) fn scale(a: V, s: f32) -> V { +pub(in crate::physics) fn scale(a: V, s: f32) -> V { a.map(|v| v * s) } -pub(super) fn madd(a: V, s: f32, b: V) -> V { +pub(in crate::physics) fn madd(a: V, s: f32, b: V) -> V { std::array::from_fn(|i| a[i].mul_add(s, b[i])) } -pub(super) fn reciprocal(s: f32) -> f32 { +pub(in crate::physics) fn reciprocal(s: f32) -> f32 { let mut r = s.recip(); for _ in 0..2 { r = (-s).mul_add(r, 1.0).mul_add(r, r); @@ -31,12 +34,12 @@ fn rsqrt(s: f32) -> f32 { } r } -pub(super) fn length(a: V) -> f32 { +pub(in crate::physics) fn length(a: V) -> f32 { let s = dot(a, a); let l = s * rsqrt(s); if s == 0.0 { 0.0 } else { l } } -pub(super) fn normalize(a: V) -> V { +pub(in crate::physics) fn normalize(a: V) -> V { let s = dot(a, a); let r = rsqrt(s); let l = if s == 0.0 { 0.0 } else { s * r }; @@ -46,21 +49,21 @@ pub(super) fn normalize(a: V) -> V { [0.0; 4] } } -pub(super) fn point(m: &[V; 4], p: V) -> V { +pub(in crate::physics) fn point(m: &[V; 4], p: V) -> V { std::array::from_fn(|i| { let x = m[0][i].mul_add(p[0], m[3][i]); let y = m[1][i].mul_add(p[1], x); m[2][i].mul_add(p[2], y) }) } -pub(super) fn rotate(m: &[V; 4], v: V) -> V { +pub(in crate::physics) fn rotate(m: &[V; 4], v: V) -> V { std::array::from_fn(|i| { let x = m[0][i] * v[0]; let y = m[1][i].mul_add(v[1], x); m[2][i].mul_add(v[2], y) }) } -pub(super) fn clamp_length(v: V, maximum: f32) -> V { +pub(in crate::physics) fn clamp_length(v: V, maximum: f32) -> V { let l = length(v); if !(l >= f32::from_bits(0x37800000)) { return v; @@ -68,7 +71,7 @@ pub(super) fn clamp_length(v: V, maximum: f32) -> V { let capped = if maximum - l >= 0.0 { l } else { maximum }; scale(scale(v, capped), reciprocal(l)) } -pub(super) fn clamp01(v: f32) -> f32 { +pub(in crate::physics) fn clamp01(v: f32) -> f32 { let a = if -v >= 0.0 { 0.0 } else { v }; if 1.0 - a >= 0.0 { a } else { 1.0 } } diff --git a/crates/skate-game/src/physics/footplant/settings.rs b/crates/skate-game/src/physics/footplant/settings.rs index 4d831416..c1b4913e 100644 --- a/crates/skate-game/src/physics/footplant/settings.rs +++ b/crates/skate-game/src/physics/footplant/settings.rs @@ -1,15 +1,27 @@ //! Cached physics_footplantmanager layout used by82D6FD60/82D6F5F0/82D70070. use skate_data::collections::Collections; +use skate_core::point_graph::PointGraph; pub(super) struct Settings { pub deck_bounds: [f32; 4], //0 pub max_leg_angle_error: f32, //280 degrees pub leg_length_on_landing: f32, //288 pub foot_volume_y_offset: f32, //296 pub deck_bounds_y_offset: f32, //300 + pub radial_speed_scale: PointGraph<8>, //16 + pub max_horizontal_speed: f32, //256 + pub max_descending_speed: f32, //260 + pub release_outward_speed: f32, //268 + pub end_handle: f32, //272 + pub end_angle: f32, //276 degrees + pub end_leg_length: f32, //284 + pub start_handle: f32, //292 + pub min_duration: f32, //304 + pub max_duration: f32, //308 } impl Settings { pub fn load(data: &Collections) -> Result { let float = |name| data.float("physics_footplantmanager", "default", name); + let graph = data.words::<20>("physics_footplantmanager", "default", "Hash_8A8887218466ED15")?.map(f32::from_bits); Ok(Self { deck_bounds: data .words::<4>("physics_footplantmanager", "default", "DeckBB")? @@ -18,6 +30,16 @@ impl Settings { leg_length_on_landing: float("LegLengthOnLanding")?, foot_volume_y_offset: float("FootVolumeYOffset")?, deck_bounds_y_offset: float("DeckBBYOffset")?, + radial_speed_scale: PointGraph { x: graph[4..12].try_into().unwrap(), y: graph[12..20].try_into().unwrap() }, + max_horizontal_speed: float("Hash_149A5D0D4768D2B1")?, + max_descending_speed: float("Hash_52436D9F4FE56BCB")?, + release_outward_speed: float("Hash_C5F92168DFAFD0B8")?, + end_handle: float("Hash_628C228DCC7B6ED5")?, + end_angle: float("Hash_4F45DA86685CF029")?, + end_leg_length: float("Hash_0DE0027B9913C5D5")?, + start_handle: float("Hash_5C4341EF35F4B2B0")?, + min_duration: float("Hash_D6224B710A36D643")?, + max_duration: float("Hash_68C05A7FCC6BB718")?, }) } } diff --git a/crates/skate-game/src/physics/footplant/update.rs b/crates/skate-game/src/physics/footplant/update.rs index 93d0a605..b41fa13d 100644 --- a/crates/skate-game/src/physics/footplant/update.rs +++ b/crates/skate-game/src/physics/footplant/update.rs @@ -22,6 +22,7 @@ pub(crate) struct FootplantFrame<'a> { pub body: &'a SkeletonBody, pub gravity: V, pub world: &'a BoardWorld, + pub edges: &'a [skate_core::physics::grind_contact::Primitive], } impl Footplant { pub fn update_candidate(&mut self, input: &KnownAirFootplantInput, frame: &FootplantFrame<'_>) { @@ -106,7 +107,7 @@ impl Footplant { self.contact = self.result.contact_position; self.surface = self.result.surface; self.hit = true; - //82D6FBB0's nearby-edge scan is empty in our explicitly edge-free BoardWorld. + self.nearby_edge(frame); self.adjusted_contact = madd( input.landing_normal, self.settings.foot_volume_y_offset, @@ -134,28 +135,61 @@ impl Footplant { } } self.current_up = frame.processed.vectors_544_560_592_608[0].map(f32::from_bits); + let submitted = Trajectory { + position: self.request.position, velocity: self.request.velocity, + acceleration: frame.gravity, duration: 1.0, + }; let next = AirTrajectoryRuntime::query( frame.world, QueryRequest { - trajectory: Trajectory { - position: self.request.position, - velocity: self.request.velocity, - acceleration: frame.gravity, - duration: 1.0, - }, + trajectory: submitted, radius: f32::from_bits(0x3d23d70a), start_error: 1.0, end_error: 1.0, }, )?; self.result = next; + self.completed_trajectory = submitted; self.enabled = true; Ok(()) } fn clear_contact(&mut self) { self.surface = 0; self.hit = false; - self.contact_time = -1.0; + self.contact_time = 0.0; //82D6FA80/82D6FAC4, distinct from Reset's -1. self.contact = [0.0; 4]; } + + ///82D6FBB0: query the real edge geometry within the native two-metre box. + fn nearby_edge(&mut self, frame: &FootplantFrame<'_>) { + use skate_core::{math::Vector3, player::offboard::{air_ledge::visible_edges, ground_query::Edge}}; + let xyz = |v: V| Vector3::new(v[0], v[1], v[2]); + let lanes = |v: Vector3| [v.x, v.y, v.z, 0.0]; + let edges: Vec<_> = frame.edges.iter().filter(|e| (0..3).all(|i| + e.start[i].min(e.end[i]) <= self.contact[i] + 2.0 && e.start[i].max(e.end[i]) >= self.contact[i] - 2.0 + )).map(|e| Edge { start: xyz(e.start), end: xyz(e.end) }).collect(); + let original_height = self.contact[1]; + let mut best = 0.25; //820C6D98 + for edge in visible_edges(&edges, frame.toolkit.deck[3]) { + let start = lanes(edge.start); + let delta = sub(lanes(edge.end), start); + let raw_normal = cross(cross(delta, [0.0,1.0,0.0,0.0]), delta); + let normal = if length(raw_normal) > f32::from_bits(0x3586_37bd) { normalize(raw_normal) } else { [0.0,1.0,0.0,0.0] }; + let Some(time) = skate_core::air::trajectory::grind::descending_plane_time(self.completed_trajectory, start, normal) else { continue; }; + let arc = self.completed_trajectory.position_at(time); + let edge_length = length(delta); + let direction = if edge_length > f32::from_bits(0x3780_0000) { scale(delta, reciprocal(edge_length)) } else { delta }; + let point = madd(direction, dot(direction, sub(arc, start)).min(edge_length).max(0.0), start); + let distance = length(sub(arc, point)); + //Globals3644 at822F943C permits at most 3cm below the triangle hit. + if distance < best && point[1] - original_height > f32::from_bits(0xbcf5_c28f) { + self.contact_time = time; + self.current_up = normal; + self.contact = point; + self.surface = 0; + self.hit = true; + best = distance; + } + } + } } diff --git a/crates/skate-game/src/physics/frame.rs b/crates/skate-game/src/physics/frame.rs index 2c9b97a1..068a26f2 100644 --- a/crates/skate-game/src/physics/frame.rs +++ b/crates/skate-game/src/physics/frame.rs @@ -104,6 +104,9 @@ pub(super) fn advance( } player_state::pre_state(physics, skater)?; match skater.player_state.current() { + skate_core::player::state::PhysicalStateId::HandPlant => super::handplant::update(physics,skater)?, + skate_core::player::state::PhysicalStateId::FootPlant => super::footplant::ground::update(physics, skater)?, + skate_core::player::state::PhysicalStateId::Boneless => super::boneless::update(physics, skater)?, skate_core::player::state::PhysicalStateId::GrindBoardslide | skate_core::player::state::PhysicalStateId::GrindFiftyFifty | skate_core::player::state::PhysicalStateId::GrindTipslide @@ -134,6 +137,7 @@ pub(super) fn advance( //after Reckoning; this frame's propulsion may then set it again. skater.ground.state.push_suppressed_2730 = false; ground_phase::advance(physics, skater)?; + super::handplant::ground_update(physics,skater)?; input_phase::update_ground(physics, skater)?; } skate_core::player::state::PhysicalStateId::PhysicsAir => { diff --git a/crates/skate-game/src/physics/ground_phase.rs b/crates/skate-game/src/physics/ground_phase.rs index d7535a91..03b8a855 100644 --- a/crates/skate-game/src/physics/ground_phase.rs +++ b/crates/skate-game/src/physics/ground_phase.rs @@ -164,6 +164,7 @@ pub(crate) fn advance( physics: &mut GamePhysics, skater: &mut SkaterRuntime, ) -> Result { + super::handplant::ground_query(physics,skater); let p = &skater.player_input.processed; let toolkit = skater .player_input diff --git a/crates/skate-game/src/physics/handplant.rs b/crates/skate-game/src/physics/handplant.rs new file mode 100644 index 00000000..05211f96 --- /dev/null +++ b/crates/skate-game/src/physics/handplant.rs @@ -0,0 +1,271 @@ +//! TU3 HandPlantManager82D61040 and physical state600 (823273CC). +mod contact; +mod ik; +mod rotation; +mod settings; +mod trajectory; +use super::footplant::math; +use super::{GamePhysics, SkaterRuntime, plant_skeleton}; +use math::*; +use settings::Settings; +use skate_core::{air::trajectory::Trajectory, physics::skeleton_animation_record::IDENTITY}; +type V = [f32; 4]; +const UP: V = [0.0, 1.0, 0.0, 0.0]; +const GRAVITY: V = [0.0, f32::from_bits(0xc11ccccd), 0.0, 0.0]; +const EMPTY: Trajectory = Trajectory { + position: [0.0; 4], + velocity: [0.0; 4], + acceleration: [0.0; 4], + duration: -1.0, +}; + +pub(crate) struct Handplant { + settings: Settings, + pub flags: u32, + pub phase: f32, + pub anchor: V, + previous_candidate_point: V, //2192; separate from the published anchor1856. + pending: Option<(contact::Candidate, V, V, V, V)>, + candidate: Option, + initial: Trajectory, + entry: Trajectory, + outgoing: [Trajectory; 2], + curve: [V; 4], + rotations: [[V; 4]; 4], + direction: V, + travel_sign: f32, + elapsed: f32, + warped: f32, + apex: f32, + estimated_phase: f32, //2944; retained when the authored mean is zero. + out_duration: f32, + pub continuation: bool, + direction_hint: i32, + direction_count: i32, + ik_blend: f32, + ik_distance: f32, + ik_released: bool, + ik_latched: bool, +} +impl Handplant { + pub fn load(data: &skate_data::collections::Collections) -> Result { + Ok(Self { + settings: Settings::load(data)?, + flags: 0, + phase: f32::MAX, + anchor: [0.0; 4], + previous_candidate_point: [0.0; 4], + pending: None, + candidate: None, + initial: EMPTY, + entry: EMPTY, + outgoing: [EMPTY; 2], + curve: [[0.0; 4]; 4], + rotations: [IDENTITY; 4], + direction: [0.0; 4], + travel_sign: 0.0, + elapsed: -1.0, + warped: -1.0, + apex: -1.0, + estimated_phase: -1.0, + out_duration: 0.0, + continuation: true, + direction_hint: 0, + direction_count: 0, + ik_blend: 0.0, + ik_distance: -1.0, + ik_released: false, + ik_latched: false, + }) + } + pub fn animation_thresholds(&self) -> [f32; 3] { + self.settings.animation + } + ///82D62F20 preserves the new-position bit and the stored trajectories. + pub fn reset(&mut self) { + self.flags &= !0xb000_0000; + self.phase = f32::MAX; + self.anchor = [0.0; 4]; + self.pending = None; + self.candidate = None; + self.elapsed = -1.0; + self.warped = -1.0; + self.apex = -1.0; + self.estimated_phase = -1.0; + self.out_duration = 0.0; + self.direction = [0.0; 4]; + self.continuation = true; + } + pub fn full_reset(&mut self) { + self.reset(); + self.reset_ik(); + self.direction_hint = 0; + self.direction_count = 0; + } + fn reset_ik(&mut self) { + self.ik_blend = 0.0; + self.ik_distance = -1.0; + self.ik_released = false; + self.ik_latched = false; + } + fn estimate_apex(&mut self) { + //82D63280 averages four authored time-warp samples. + let offset = self.warped - self.apex; + let stride = (offset * f32::from_bits(0x3eaaaaab)).abs(); //Globals440 + let mean = (0..4) + .map(|i| 1.0 + self.settings.time_warp.evaluate(offset + i as f32 * stride)) + .sum::() + * 0.25; + if mean != 0.0 { + self.estimated_phase = (-1.0 / mean) * offset; + } + self.phase = self.estimated_phase; + } +} +pub(super) fn apex_time(t: Trajectory) -> f32 { + if t.velocity[1] < 0.0 || t.acceleration[1] >= 0.0 { + 0.0 + } else { + t.velocity[1] * reciprocal(-t.acceleration[1]) + } +} + +///Ground82D37F38 consumes the candidate query before its shared skeleton update. +pub(super) fn ground_update( + physics: &mut GamePhysics, + skater: &mut SkaterRuntime, +) -> Result<(), String> { + let p = &skater.player_input.processed; + if p.flags_2476 & (1 << 22) == 0 { + skater.handplant.full_reset(); + return Ok(()); + } + if let Some((candidate, com, velocity, normal, heading)) = skater.handplant.pending.take() { + let surface = skate_core::air::trajectory::grind_surface::investigate( + &physics.world, + candidate.edge.start, + candidate.edge.end, + candidate.point, + skater.handplant.settings.truck_distance, + )?; + if surface.is_some_and(|s| s.evidence.kind != 3) { + skater.handplant.launch( + candidate, + com, + velocity, + normal, + heading, + physics.riding.reckoning_frames.heading, + ); + } + } + if skater.handplant.flags & 0x8000_0000 != 0 { + ik::update(skater, true); + } + Ok(()) +} +///Ground82D38430 submits the candidate before82D37F38 consumes its investigation. +pub(super) fn ground_query(physics: &GamePhysics, skater: &mut SkaterRuntime) { + let p = &skater.player_input.processed; + if p.flags_2476 & (1 << 22) == 0 { + skater.handplant.full_reset(); + return; + } + let h = &mut skater.handplant; + let com = p.vectors_544_560_592_608[2].map(f32::from_bits); + let velocity = p.vectors_400_416[0].map(f32::from_bits); + let normal = p.vectors_464_480_496_512_528[0].map(f32::from_bits); + let heading = p.vectors_544_560_592_608[1].map(f32::from_bits); + if p.flags_2480 & (1 << 29) != 0 || p.flags_2480 & (1 << 28) == 0 { + h.direction_hint = 0; + h.direction_count = 0; + } else if normal[1] < 0.95 { + if h.direction_hint == 0 { + h.direction_hint = h.candidate.map_or(0, |c| c.side); + } + let side = if dot(velocity, normalize(cross(UP, normal))) > 0.0 { + 1 + } else { + 2 + }; + if h.direction_hint == side { + h.direction_count = 0; + } else { + h.direction_count += 1; + if h.direction_count > h.settings.direction_frames { + h.direction_hint = 0; + } + } + } else { + h.direction_hint = 0; + h.direction_count = 0; + } + let candidate = contact::select( + &h.settings, + com, + velocity, + normal, + h.direction_hint, + &physics.grind.primitives, + ); + //82D61268 clears the active output when submission begins. + let old_point = h.candidate.map_or([0.0; 4], |c| c.point); + let hint = h.direction_hint; + let count = h.direction_count; + h.reset(); + h.previous_candidate_point = old_point; + h.direction_hint = hint; + h.direction_count = count; + h.candidate = candidate; + h.pending = candidate.map(|c| (c, com, velocity, normal, heading)); +} +pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + physics + .board + .hook_mut() + .drive + .enable_angular_only(&mut skater.ground_lifecycle.board_animated_290); + let p = &skater.player_input.processed; + let h = &mut skater.handplant; + h.entry = Trajectory { + position: p.vectors_544_560_592_608[2].map(f32::from_bits), + velocity: p.vectors_544_560_592_608[3].map(f32::from_bits), + acceleration: GRAVITY, + duration: -1.0, + }; + h.warped = 0.0; + h.elapsed = 0.0; + h.continuation = true; + h.select_outgoing(&physics.world)?; + Ok(()) +} +pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + //82D4C3D8 ->82D91298 disables five upper-body volumes for two frames. + for part in [8, 4, 9, 5, 2] { + skater.skeleton_collision.pending_reenable = true; + skater.skeleton_collision.disable_count[part] = 2; + skater.skeleton_collision.parts[part].enabled = false; + } + let h = &mut skater.handplant; + h.warped += (1.0 + h.settings.time_warp.evaluate(h.warped - h.apex)) * STEP; + h.estimate_apex(); + h.elapsed += STEP; + let pose = skater.animated_skeleton.record.pose.map(|local| { + skate_core::physics::skeleton_animation_record::compose_affine( + &skater.animated_skeleton.roots.animation_to_world, + &local, + ) + }); + let (com, up, heading) = h.values( + &pose, + skater.player_input.processed.vectors_544_560_592_608[2].map(f32::from_bits), + ); + skater.air_reckoning.update_plant( + &mut physics.riding, + &skater.player_input.processed, + up, + heading, + ); + ik::update(skater, false); + plant_skeleton::advance(physics, skater, com, None) +} diff --git a/crates/skate-game/src/physics/handplant/contact.rs b/crates/skate-game/src/physics/handplant/contact.rs new file mode 100644 index 00000000..383cee75 --- /dev/null +++ b/crates/skate-game/src/physics/handplant/contact.rs @@ -0,0 +1,242 @@ +//! PositionSelector82D63D40, velocity window82D64420 and edge clipping82D651E0. +use super::{GRAVITY, Settings, UP, V, math::*}; +use skate_core::animation::foot_ik::transforms::inverse_rigid; +use skate_core::{ + air::trajectory::Trajectory, + physics::{grind_contact::Primitive, skeleton_animation_record::IDENTITY}, +}; + +#[derive(Clone, Copy)] +pub(super) struct Candidate { + pub point: V, + pub edge: Primitive, + pub side: i32, +} + +pub(super) fn select( + settings: &Settings, + com: V, + velocity: V, + normal: V, + hint: i32, + edges: &[Primitive], +) -> Option { + //82D61D88: ascending, above minimum speed, slope over45deg and |Ny|<=.95. + if !(length(velocity) > settings.minimum_speed + && velocity[1] > 0.0 + && normal[1].abs() <= 0.95 + && skate_core::trigonometry::acos(normal[1].clamp(-1.0, 1.0)) * f32::from_bits(0x42652ee1) + > settings.minimum_slope) + { + return None; + } + let mut frame = IDENTITY; + if normal[1].abs() <= 0.99 { + frame[0] = normalize(cross(UP, normal)); + frame[1] = UP; + frame[2] = cross(frame[0], UP); + } + frame[3] = com; + let inverse = inverse_rigid(&frame); + let mut velocity = rotate(&inverse, velocity); + velocity = [ + velocity[0], + (velocity[0] * velocity[0] + velocity[1] * velocity[1]).sqrt(), + 0.0, + 0.0, + ]; + if hint != 0 && hint != if velocity[0] > 0.0 { 1 } else { 2 } { + velocity[0] = if hint == 1 { 1.0 } else { -1.0 } * f32::from_bits(0x37800000); + } + let g = settings.window.each_ref().map(|g| g.evaluate(velocity[1])); + let lo_x = g[1].max(velocity[0].abs() - g[5]); + let hi_x = g[6].min(velocity[0].abs() + g[4]); + if hi_x <= lo_x { + return None; + } + let lo_y = g[0].max(velocity[1] - g[3]); + let hi_y = velocity[1] + g[2]; + let sign = if velocity[0] > 0.0 { 1.0 } else { -1.0 }; + let apex = |x, y| { + let trajectory = Trajectory { + position: point(&inverse, com), + velocity: [x * sign, y, 0.0, 0.0], + acceleration: GRAVITY, + duration: -1.0, + }; + let mut p = trajectory.position_at(super::apex_time(trajectory)); + p[1] -= settings.window_drop; + p + }; + //Native order:r5 lowX/lowY,r6 highX/lowY,r7 lowX/highY,r8 highX/highY. + let [a, b, c, d] = [ + apex(lo_x, lo_y), + apex(hi_x, lo_y), + apex(lo_x, hi_y), + apex(hi_x, hi_y), + ]; + let polygon = if d[0] > c[0] { + [c, d, b, a] + } else { + [d, c, a, b] + }; + //82C1EAD8 preserves world order and caps the broadphase result at40. + let mut candidates = Vec::new(); + for edge in edges + .iter() + .filter(|e| { + (0..3).all(|i| { + let extent = [2.0, 4.0, 2.0][i]; + e.start[i].min(e.end[i]) <= com[i] + extent + && e.start[i].max(e.end[i]) >= com[i] - extent + }) + }) + .take(40) + { + let start = point(&inverse, edge.start); + let end = point(&inverse, edge.end); + if normalize(sub(end, start))[0].abs() <= 0.71 { + continue; + } //82D64218 + let mut segment = [start, end]; + //82D657D0/658A0 retains -depth segment[1][1] { + segment[0] + } else { + segment[1] + }; + if p[1] > height + 0.1 || (p[1] > height - 0.1 && p[1] <= height + 0.1 && p[2] > depth) { + best = Some(i); + height = p[1]; + depth = p[2]; + } + } + let best = best?; + let mut connected = vec![false; candidates.len()]; + connected[best] = true; + //82D65BD8: extend both ends across neighbours within .05m and30degrees. + for reverse in [false, true] { + let mut ends = candidates[best].1; + if reverse { + ends.swap(0, 1); + } + loop { + let next = candidates.iter().enumerate().find_map(|(i, (_, s))| { + if connected[i] { + return None; + } + for e in 0..2 { + if dot(sub(ends[1], s[e]), sub(ends[1], s[e])) < f32::from_bits(0x3b23d70a) + && skate_core::trigonometry::acos( + dot( + normalize(sub(ends[1], ends[0])), + normalize(sub(s[1 - e], s[e])), + ) + .clamp(-1.0, 1.0), + ) + .abs() + < f32::from_bits(0x3f060a92) + { + return Some((i, [s[e], s[1 - e]])); + } + } + None + }); + let Some((i, s)) = next else { break }; + connected[i] = true; + ends = s; + } + } + let bottom = scale(add(polygon[2], polygon[3]), 0.5); + let top = scale(add(polygon[0], polygon[1]), 0.5); + let delta = sub(top, bottom); + let target = madd( + delta, + clamp01((height - bottom[1]) * reciprocal(delta[1])), + bottom, + ); + let target = [target[0], target[1], 0.0, 0.0]; + let mut nearest = [0.0; 4]; + let mut distance = f32::MAX; + for (i, (_, segment)) in candidates.iter().enumerate() { + if !connected[i] { + continue; + } + let delta = sub(segment[1], segment[0]); + let square = dot(delta, delta); + let p = if square > f32::from_bits(0x37800000) { + madd( + delta, + clamp01(dot(sub(target, segment[0]), delta) * reciprocal(square)), + segment[0], + ) + } else { + segment[0] + }; + let square = dot(sub(p, target), sub(p, target)); + if square < distance { + distance = square; + nearest = p; + } + } + Some(Candidate { + point: point(&frame, nearest), + edge: candidates[best].0, + side: if velocity[0] > 0.0 { 1 } else { 2 }, + }) +} +//82D658A0 excludes coplanar segments and avoids dividing near-parallel Z. +fn clip_depth(segment: &mut [V; 2], value: f32, above: bool) -> bool { + let sign = if above { 1.0 } else { -1.0 }; + let a = (segment[0][2] - value) * sign; + let b = (segment[1][2] - value) * sign; + if a <= 0.0 && b <= 0.0 { + return false; + } + let delta = segment[1][2] - segment[0][2]; + if delta.abs() >= f32::from_bits(0x37800000) { + let t = (value - segment[0][2]) * reciprocal(delta); + if t > 0.0 && t < 1.0 { + let p = madd(sub(segment[1], segment[0]), t, segment[0]); + segment[usize::from(b <= 0.0)] = p; + } + } + true +} +fn clip(segment: &mut [V; 2], a: f32, b: f32) -> bool { + if a < 0.0 && b < 0.0 { + return false; + } + if (a < 0.0) != (b < 0.0) { + let p = madd( + sub(segment[1], segment[0]), + a * reciprocal(a - b), + segment[0], + ); + segment[usize::from(a >= 0.0)] = p; + } + true +} diff --git a/crates/skate-game/src/physics/handplant/ik.rs b/crates/skate-game/src/physics/handplant/ik.rs new file mode 100644 index 00000000..56e9574c --- /dev/null +++ b/crates/skate-game/src/physics/handplant/ik.rs @@ -0,0 +1,90 @@ +//! HandPlantManager82D633B0 and Skeleton82BD9728/97D0, shared four-limb IK. +use super::*; +pub(super) fn update(skater: &mut SkaterRuntime, ground: bool) { + let p = &skater.player_input.processed; + let h = &mut skater.handplant; + if ground && (p.flags_2480 & (1 << 29) != 0 || p.flags_2480 & (1 << 28) == 0) { + h.reset_ik(); + } + let right = (h.flags & 0x2000_0000 != 0) ^ (p.flags_2476 & (1 << 2) != 0); + let side = usize::from(right); + let limb = 2 + side; + let animated = &skater.animated_skeleton; + let world = |part: usize| { + point( + &animated.roots.animation_to_world, + animated.record.pose[part][3], + ) + }; + let mut hand = world(3 + side * 4); + let shoulder = world(6 + side * 4); + let attached = h.phase > -h.settings.hand_release; + if !attached { + h.ik_released = true; + } + let shoulder_delta = sub(shoulder, h.anchor); + if !h.ik_released && length(shoulder_delta) > f32::from_bits(0x37800000) { + let axis = normalize(shoulder_delta); + let projection = dot(sub(hand, h.anchor), axis); + if projection <= 0.0 { + h.ik_released = true; + } else { + let on_axis = madd(axis, projection, h.anchor); + if h.ik_latched { + hand = on_axis; + } else { + let radial = sub(hand, on_axis); + let radius = h.settings.hand_radius.evaluate(projection); + if length(radial) > radius { + hand = madd(normalize(radial), radius, on_axis); + } + } + } + let delta = sub(hand, h.anchor); + let distance = length(delta); + if h.ik_distance < 0.0 { + h.ik_distance = distance; + } + h.ik_distance = distance.min(h.ik_distance - h.settings.hand_approach); + if h.ik_distance < f32::from_bits(0x37800000) || distance < f32::from_bits(0x37800000) { + h.ik_released = true; + } else { + hand = madd(normalize(delta), h.ik_distance, h.anchor); + } + if !h.ik_released && !h.ik_latched { + let on_axis = madd(axis, dot(sub(hand, h.anchor), axis), h.anchor); + if length(sub(hand, on_axis)) < 0.02 { + h.ik_latched = true; + } + } + } + if h.ik_released { + hand = h.anchor; + } + let delta = sub(hand, shoulder); + if length(delta) > 0.65 { + hand = madd(normalize(delta), 0.65, shoulder); + } + h.ik_blend = clamp01( + h.ik_blend + + if attached { + STEP / h.settings.hand_into + } else { + -STEP / h.settings.hand_out + }, + ); + if h.ik_blend > 0.0 { + //82BD9728 clamps against the authored reparented hand target. + let original = point( + &animated.roots.animation_to_world, + animated.targets[limb][3], + ); + let delta = sub(hand, original); + if length(delta) > 0.65 { + hand = madd(normalize(delta), 0.65, original); + } + skater.foot_ik.state.external_targets[limb].world_position = hand; + skater.foot_ik.state.limbs[limb].external_target_set = true; + skater.foot_ik.state.limbs[limb].target_blend = h.ik_blend; + } +} diff --git a/crates/skate-game/src/physics/handplant/rotation.rs b/crates/skate-game/src/physics/handplant/rotation.rs new file mode 100644 index 00000000..1baedd35 --- /dev/null +++ b/crates/skate-game/src/physics/handplant/rotation.rs @@ -0,0 +1,50 @@ +//! CreateMtxFromHeadingAndUp82D61B60 and axis-angle frame blend82BD3150. +use super::*; +use skate_core::animation::foot_ik::transforms::inverse_rigid; +use skate_core::math::Basis3; +use skate_core::physics::{ + drive_frames::retail_quaternion_from_basis, skeleton_animation_record::compose_affine, +}; +pub(super) fn frame(mut heading: V, up: V) -> [V; 4] { + if dot(heading, up).abs() > f32::from_bits(0x3f7fff58) { + heading = [1.0, 0.0, 0.0, 0.0]; + if dot(heading, up).abs() > f32::from_bits(0x3f7fff58) { + heading = [0.0, 0.0, 1.0, 0.0]; + } + } + let right = normalize(cross(up, heading)); + let forward = normalize(cross(right, up)); + [right, normalize(cross(forward, right)), forward, [0.0; 4]] +} +pub(super) fn blend(a: [V; 4], b: [V; 4], weight: f32) -> [V; 4] { + if weight >= 1.0 { + return b; + } + if weight <= 0.0 { + return a; + } + let relative = compose_affine(&b, &inverse_rigid(&a)); + let q = retail_quaternion_from_basis(Basis3 { + columns: std::array::from_fn(|i| relative[i][..3].try_into().unwrap()), + }); + let sign = if q.w < 0.0 { -1.0 } else { 1.0 }; + let angle = 2.0 * skate_core::trigonometry::acos((q.w * sign).clamp(-1.0, 1.0)); + let mut result = a; + if angle < f32::from_bits(0x3d0efa35) { + for i in 0..3 { + result[i] = normalize(madd(sub(b[i], a[i]), weight, a[i])); + } + } else { + let axis = normalize([q.x * sign, q.y * sign, q.z * sign, 0.0]); + let (sin, cos) = skate_core::trigonometry::sin_cos(angle * weight); + for i in 0..3 { + result[i] = madd( + axis, + dot(axis, a[i]) * (1.0 - cos), + madd(cross(axis, a[i]), sin, scale(a[i], cos)), + ); + } + } + result[3] = madd(sub(b[3], a[3]), weight, a[3]); + result +} diff --git a/crates/skate-game/src/physics/handplant/settings.rs b/crates/skate-game/src/physics/handplant/settings.rs new file mode 100644 index 00000000..06cafa90 --- /dev/null +++ b/crates/skate-game/src/physics/handplant/settings.rs @@ -0,0 +1,96 @@ +//! TU3 cached layouts55B5940075F78842 and0F38084037CB9175. +use skate_core::point_graph::PointGraph; +use skate_data::collections::Collections; + +pub(super) struct Settings { + pub window: [PointGraph<4>; 7], + pub depth: f32, + pub window_drop: f32, + pub time_warp: PointGraph<8>, + pub out_heading: PointGraph<8>, + pub into_rotation: PointGraph<8>, + pub out_rotation: PointGraph<8>, + pub hand_radius: PointGraph<4>, + pub curve_half_time: f32, + pub entry_blend: f32, + pub hand_out: f32, + pub hand_into: f32, + pub hand_release: f32, + pub minimum_speed: f32, + pub minimum_slope: f32, + pub rotation_time: f32, + pub committed_time: f32, + pub minimum_out_speed: f32, + pub hand_approach: f32, + pub direction_frames: i32, + pub apex_radius: f32, + pub apex_angle: f32, + pub animation: [f32; 3], + pub truck_distance: f32, +} +impl Settings { + pub fn load(data: &Collections) -> Result { + let root = "Hash_55B5940075F78842"; + let graph4 = |class, name| -> Result, String> { + let w = data + .words::<12>(class, "default", name)? + .map(f32::from_bits); + Ok(PointGraph { + x: w[4..8].try_into().unwrap(), + y: w[8..12].try_into().unwrap(), + }) + }; + let graph8 = |name| -> Result, String> { + let w = data + .words::<20>("physics_handplantmanager", "default", name)? + .map(f32::from_bits); + Ok(PointGraph { + x: w[4..12].try_into().unwrap(), + y: w[12..20].try_into().unwrap(), + }) + }; + let f = |name| data.float("physics_handplantmanager", "default", name); + Ok(Self { + truck_distance: data.float("physics_grinds", "default", "DeckCenterToTruck")?, + window: [ + graph4(root, "Hash_FF0DE8A42210E8C0")?, + graph4(root, "Hash_B90C78786ED04A7D")?, + graph4(root, "Hash_DC7947C6DD03CDAA")?, + graph4(root, "Hash_0ACB7AAE2662FBB2")?, + graph4(root, "Hash_CF7F668AB995380D")?, + graph4(root, "Hash_8348DA199CC87B7A")?, + graph4(root, "Hash_F27F622CAE33BD50")?, + ], + depth: data.float(root, "default", "Hash_3D9138A04C6D43F6")?, + window_drop: data.float(root, "default", "Hash_A046D0516C0BD62B")?, + time_warp: graph8("Hash_F551DF3289124FB0")?, + out_heading: graph8("Hash_8FC9A4F9002CF9E0")?, + into_rotation: graph8("Hash_F61B0FB73C39311B")?, + out_rotation: graph8("Hash_D4EF764AF4D5F1CA")?, + hand_radius: graph4("physics_handplantmanager", "Hash_5494E3F9BE572D6B")?, + curve_half_time: f("Hash_2460D6544C3B64B7")?, + entry_blend: f("Hash_CCD7A82CE1BBFFF2")?, + hand_out: f("Hash_E0895C189D77CE16")?, + hand_into: f("Hash_01EB855A0ACBD84C")?, + hand_release: f("Hash_9C02B9E6998D1321")?, + minimum_speed: f("Hash_63303CA6C611DC79")?, + minimum_slope: f("Hash_D1622917F6DD215F")?, + rotation_time: f("Hash_D595A32C5F02178F")?, + committed_time: f("Hash_B5EC5A0941B407A4")?, + minimum_out_speed: f("Hash_49C91803479B9BED")?, + hand_approach: f("Hash_7C8B87F7435221C4")?, + direction_frames: data.words::<1>( + "physics_handplantmanager", + "default", + "Hash_DDE5BFBCB6A1AFD5", + )?[0] as i32, + apex_radius: f("Hash_A046D0516C0BD62B")?, + apex_angle: f("Hash_90CDD541098AB4B5")?, + animation: [ + data.float("anim_handplant", "default", "Hash_B9012B86EB5258FE")?, + data.float("anim_handplant", "default", "Hash_512342FB1D4A7D57")?, + data.float("anim_handplant", "default", "Hash_673F3B1BD056C39E")?, + ], + }) + } +} diff --git a/crates/skate-game/src/physics/handplant/trajectory.rs b/crates/skate-game/src/physics/handplant/trajectory.rs new file mode 100644 index 00000000..528a7cb0 --- /dev/null +++ b/crates/skate-game/src/physics/handplant/trajectory.rs @@ -0,0 +1,269 @@ +//! Handplant Launch82D613B0, trajectory selector82D66318/66948 and COM82D625C0. +use super::*; +use skate_core::{air::trajectory::QueryRequest, physics::board_world::BoardWorld}; + +impl Handplant { + pub(super) fn launch( + &mut self, + c: contact::Candidate, + com: V, + velocity: V, + normal: V, + body_heading: V, + heading: V, + ) { + let mut side = normalize(cross(sub(c.edge.end, c.edge.start), UP)); + if dot(side, normal) > 0.0 { + side = scale(side, -1.0); + } + self.direction = scale(side, -1.0); + let (sin, cos) = skate_core::trigonometry::sin_cos(self.settings.apex_angle); + let apex = madd( + side, + self.settings.apex_radius * cos, + madd(UP, self.settings.apex_radius * sin, c.point), + ); + self.initial = to_apex(com, apex); + self.apex = apex_time(self.initial); + self.warped = 0.0; + self.outgoing = [self.initial; 2]; + self.travel_sign = if dot(heading, velocity) > 0.0 { + 1.0 + } else { + -1.0 + }; + //830BD4A0=-1 and830BD320=1; signs follow native vsel order. + self.rotations[0] = rotation::frame(heading, normal); + self.rotations[0][3] = com; + let mut along = normalize(sub(c.edge.start, c.edge.end)); + let direction = if dot([along[0], 0.0, along[2], 0.0], sub(apex, com)) > 0.0 { + 1.0 + } else { + -1.0 + }; + along = scale(along, direction * self.travel_sign); + self.rotations[1] = rotation::frame(along, normalize(sub(c.point, apex))); + self.rotations[1][3] = apex; + self.rotations[2] = rotation::frame(scale(UP, -self.travel_sign), normal); + self.rotations[3] = self.rotations[2]; + self.estimate_apex(); + let changed = length(sub(self.previous_candidate_point, c.point)) > 0.15; + let front = dot(sub(c.point, com), body_heading) < 0.0; + self.flags = (self.flags & 0x0fff_ffff) + | 0x8000_0000 + | if changed { 0x4000_0000 } else { 0 } + | if front { 0x2000_0000 } else { 0 } + | if self.phase < self.settings.committed_time { + 0x1000_0000 + } else { + 0 + }; + self.anchor = c.point; + self.candidate = Some(c); + } + pub(super) fn select_outgoing(&mut self, world: &BoardWorld) -> Result<(), String> { + let candidate = self + .candidate + .ok_or("Handplant entry requires the selected coping")?; + let apex = self.initial.position_at(self.apex); + let incoming = self.initial.velocity_at(self.apex); + let across = scale(self.direction, dot(incoming, self.direction)); + let tangent = sub(incoming, across); + let along = normalize(tangent); + let speed = length(add(across, clamp_length(tangent, 2.0))); + if candidate.side != 0 { + let expected = scale( + cross(UP, self.direction), + if candidate.side == 1 { 1.0 } else { -1.0 }, + ); + self.continuation = dot(expected, along) >= 0.0; + } + let mut position = madd(self.direction, 0.1, self.anchor); + position[1] = apex[1]; + let mut selected = None; + let mut best_up = 1.1; + let mut final_trajectory = EMPTY; + for angle in [ + 0.0, + 0.24, + 0.48, + f32::from_bits(0x3f3851eb), + 0.96, + f32::from_bits(0x3f999999), + ] { + let (sin, cos) = skate_core::trigonometry::sin_cos(angle); + let velocity = scale(madd(self.direction, sin, scale(along, cos)), speed); + let trajectory = Trajectory { + position, + velocity, + acceleration: GRAVITY, + duration: 3.0, + }; + final_trajectory = trajectory; + let hit = super::super::air_trajectory::AirTrajectoryRuntime::query( + world, + QueryRequest { + trajectory, + radius: 0.2, + start_error: 1.0, + end_error: 1.0, + }, + )?; + if hit.contact_time < 0.0 { + continue; + } + let point = hit.contact_position; //PredictionResults::GetPosition82D2D988. + if self.anchor[1] - point[1] <= 0.5 { + continue; + } + let normal = hit.landing_normal; + if normal[1] < 0.71 { + selected = Some((point, normal)); + break; + } + if normal[1] < best_up { + best_up = normal[1]; + selected = Some((point, normal)); + } + } + let (landing, normal) = selected.unwrap_or((final_trajectory.position_at(3.0), UP)); + let destination = madd(normal, 0.56, landing); + let mut outgoing = from_apex(apex, destination); + let fall_time = outgoing.duration; + //82D66D70 rebase the outgoing parabola to the incoming time origin. + outgoing.position = outgoing.position_at(-self.apex); + outgoing.velocity = outgoing.velocity_at(-self.apex); + outgoing.duration = -1.0; + self.outgoing = [outgoing; 2]; + let speed = dot(self.direction, self.outgoing[1].velocity); + if speed < self.settings.minimum_out_speed { + self.outgoing[1].velocity = madd( + self.direction, + self.settings.minimum_out_speed - speed, + self.outgoing[1].velocity, + ); + } + self.rotations[2][3] = outgoing.position_at(2.0 * self.apex); + let time = self.apex + fall_time; + self.rotations[3] = rotation::frame( + scale(normalize(outgoing.velocity_at(time)), self.travel_sign), + normal, + ); + self.rotations[3][3] = outgoing.position_at(time); + self.out_duration = (time - self.phase).max(0.5); + self.build_curve(); + Ok(()) + } + fn build_curve(&mut self) { + //82D62408: secant handles one fixed tick apart, scaled by6. + let start = self.apex - self.settings.curve_half_time; + let end = self.apex + self.settings.curve_half_time; + self.curve[0] = self.initial.position_at(start); + self.curve[1] = madd( + sub(self.initial.position_at(start + STEP), self.curve[0]), + 6.0, + self.curve[0], + ); + self.curve[3] = self.outgoing[1].position_at(end); + self.curve[2] = madd( + sub(self.outgoing[1].position_at(end - STEP), self.curve[3]), + 6.0, + self.curve[3], + ); + } + pub(super) fn values(&mut self, pose: &[[V; 4]], physical_com: V) -> (V, V, V) { + let relative = self.warped - self.apex; + let fraction = (1.0 + relative * reciprocal(self.settings.curve_half_time)) * 0.5; + let mut com = if fraction < 0.0 { + self.initial.position_at(self.warped) + } else if fraction > 1.0 { + let blend = + clamp01((relative - self.settings.curve_half_time) * reciprocal(self.out_duration)); + madd( + self.outgoing[0].position_at(self.warped), + blend, + scale(self.outgoing[1].position_at(self.warped), 1.0 - blend), + ) + } else { + bezier(self.curve, fraction) + }; + let foot_y = pose[15][3][1].max(pose[19][3][1]); + let (divisor, offset) = if self.flags & 0x2000_0000 != 0 { + (5.0, 0.0) + } else { + (3.2, -0.05) + }; + com[1] += ((foot_y - physical_com[1]) / divisor + offset).max(0.0); + let blend = (1.0 - self.elapsed * reciprocal(self.settings.entry_blend)).max(0.0); + let blended = madd( + com, + 1.0 - blend, + scale(self.entry.position_at(self.elapsed), blend), + ); + if blend > 0.0 && fraction < 0.0 { + let velocity = self.initial.velocity_at(self.warped); + let correction = + dot(sub(blended, com), normalize(velocity)) * reciprocal(length(velocity)); + self.warped += correction.clamp(f32::from_bits(0xbc088889), f32::from_bits(0x3c088889)); + } + let frame = if self.warped < self.apex { + let fraction = (1.0 + - (self.apex - self.warped) + * reciprocal(self.apex.min(self.settings.rotation_time))) + .max(0.0); + rotation::blend( + self.rotations[0], + self.rotations[1], + self.settings.into_rotation.evaluate(fraction), + ) + } else { + let fraction = self.warped - self.apex; + let out = self + .settings + .out_heading + .evaluate(clamp01(fraction * reciprocal(self.out_duration))); + let into = self + .settings + .out_rotation + .evaluate(clamp01(fraction * reciprocal(self.settings.rotation_time))); + rotation::blend( + self.rotations[1], + rotation::blend(self.rotations[2], self.rotations[3], out), + into, + ) + }; + (blended, frame[1], frame[2]) + } +} +fn to_apex(start: V, end: V) -> Trajectory { + let t = ((-2.0 * GRAVITY[1]) * (end[1] - start[1])).sqrt() * reciprocal(-GRAVITY[1]); + arc_between(start, end, t) +} +fn from_apex(start: V, end: V) -> Trajectory { + let t = ((-2.0 * GRAVITY[1]) * (start[1] - end[1])).sqrt() * reciprocal(-GRAVITY[1]); + arc_between(start, end, t) +} +fn arc_between(start: V, end: V, time: f32) -> Trajectory { + let velocity = scale( + sub(sub(end, start), scale(GRAVITY, 0.5 * time * time)), + reciprocal(time), + ); + Trajectory { + position: start, + velocity, + acceleration: GRAVITY, + duration: time, + } +} +fn bezier(p: [V; 4], t: f32) -> V { + let s = 1.0 - t; + madd( + p[3], + t * t * t, + madd( + p[2], + 3.0 * t * t * s, + madd(p[0], s * s * s, scale(p[1], 3.0 * t * s * s)), + ), + ) +} diff --git a/crates/skate-game/src/physics/input_phase.rs b/crates/skate-game/src/physics/input_phase.rs index ac7788ee..bee5e8f1 100644 --- a/crates/skate-game/src/physics/input_phase.rs +++ b/crates/skate-game/src/physics/input_phase.rs @@ -58,6 +58,8 @@ pub(super) fn advance( skeleton_input: &mut skater.skeleton_input, animated: &mut skater.animated_skeleton, skeleton_air: &mut skater.skeleton_air, + footplant: &mut skater.footplant, + handplant: &mut skater.handplant, body: &mut skater.skeleton, drives: &mut skater.skeleton_drives, ik: &mut skater.foot_ik, @@ -147,6 +149,8 @@ struct Callbacks<'a, 'p> { skeleton_input: &'a mut SkeletonInputRuntime, animated: &'a mut AnimatedSkeleton, skeleton_air: &'a mut super::skeleton_air::SkeletonAir, + footplant: &'a mut super::footplant::Footplant, + handplant: &'a mut super::handplant::Handplant, body: &'a mut SkeletonBody, drives: &'a mut SkeletonDrives, ik: &'a mut FootIk, diff --git a/crates/skate-game/src/physics/input_teleport.rs b/crates/skate-game/src/physics/input_teleport.rs index 98982f55..a7ae5f94 100644 --- a/crates/skate-game/src/physics/input_teleport.rs +++ b/crates/skate-game/src/physics/input_teleport.rs @@ -44,6 +44,8 @@ impl Callbacks<'_, '_> { self.wiping_out, ); self.skeleton_air.reset_board(); //Board Reset82C0606C. + self.footplant.full_reset(); + self.handplant.full_reset(); self.riding.reset_for_teleport(); { let mut owners = SkeletonOwners { diff --git a/crates/skate-game/src/physics/known_air/runtime.rs b/crates/skate-game/src/physics/known_air/runtime.rs index ba882304..e7d20621 100644 --- a/crates/skate-game/src/physics/known_air/runtime.rs +++ b/crates/skate-game/src/physics/known_air/runtime.rs @@ -294,6 +294,7 @@ impl KnownAirRuntime for Runtime<'_> { body: &self.skater.skeleton, gravity: [g.x, g.y, g.z, 0.0], world: &self.physics.world, + edges: &self.physics.grind.primitives, }; self.skater.footplant.update_candidate(input, &f); } @@ -309,6 +310,7 @@ impl KnownAirRuntime for Runtime<'_> { body: &self.skater.skeleton, gravity: [g.x, g.y, g.z, 0.0], world: &self.physics.world, + edges: &self.physics.grind.primitives, }; let result = self.skater.footplant.consume_and_submit( input, diff --git a/crates/skate-game/src/physics/plant_skeleton.rs b/crates/skate-game/src/physics/plant_skeleton.rs new file mode 100644 index 00000000..4e500cff --- /dev/null +++ b/crates/skate-game/src/physics/plant_skeleton.rs @@ -0,0 +1,91 @@ +//! Skeleton82BDEC48 (anchored foot) and82BDF090 (plant COM). +use super::{GamePhysics, SkaterRuntime, input_phase, skeleton_input_runtime::SkeletonOwners}; +use skate_core::{ + math::Vector3, + physics::{ + board::BodyId, + board_animation::target_velocity, + skeleton_air_frames::update_plant_roots, + skeleton_animation_record::{IDENTITY, compose_affine}, + }, +}; + +pub(super) fn advance( + physics: &mut GamePhysics, + skater: &mut SkaterRuntime, + anchor: [f32; 4], + bone: Option, +) -> Result<(), String> { + let collision = input_phase::collision(skater); + let p = &mut skater.player_input.processed; + let s = &mut skater.animated_skeleton; + let local = bone.map_or(s.record.centre_of_mass, |i| { + skater.skeleton_input.drive_frames[i][3] + }); + update_plant_roots( + &mut s.roots, + &physics.riding.reckoning_frames.system, + anchor, + local, + ); + s.motion.trajectory = IDENTITY; + s.motion.inverse_trajectory = IDENTITY; + let local_board = if bone.is_some() { + &s.record.pose[0] + } else { + &skater.skeleton_input.drive_frames[0] + }; + let target = compose_affine(&s.roots.animation_to_world, local_board); + p.flags_2468 |= 1 << 19; + s.board_frames.animation_target = target; + let effective = skater + .skeleton_air + .apply_board(&mut physics.board, &target, true); + s.board_frames.physical_board = effective; + s.board_frames.skate_root = effective; + if bone.is_none() { + let pos = physics.board.bodies()[BodyId::Deck.index()].rates.position; + let velocity = target_velocity(effective[3], [pos.x, pos.y, pos.z, 0.0], p.timestep_2604); + for body in physics.board.bodies_mut() { + body.rates.linear_velocity = Vector3::new(velocity[0], velocity[1], velocity[2]); + } + } + s.board_frames.update_com_lift( + &s.roots.animation_to_world, + s.board_frames.centre_of_mass, + 0.0, + ); + let mut owners = SkeletonOwners { + animated: s, + body: &mut skater.skeleton, + drives: &mut skater.skeleton_drives, + ik: &mut skater.foot_ik, + animation_input: &mut skater.animation_input, + correction: &mut skater.skeleton_output.correction, + pose_errors: &mut skater.pose_errors, + }; + skater.skeleton_input.general_update( + p, + &mut owners, + &skater.animation.packet.hierarchy, + &collision, + physics.settings.step.simulation, + )?; + owners.animated.finish_ground(); + owners.animation_input.fields.flags2468 = p.flags_2468; + Ok(()) +} + +pub(super) fn hold_foot(skater: &mut SkaterRuntime, right: bool, position: [f32; 4], frames: u32) { + let side = usize::from(right); + skater.foot_ik.state.external_targets[side].world_position = position; + skater.foot_ik.state.limbs[side].external_target_set = true; + skater.foot_ik.state.limbs[side].target_blend = 1.0; + if frames != 0 { + for part in if right { [19, 20, 21] } else { [15, 16, 17] } { + skater.skeleton_collision.pending_reenable = true; + skater.skeleton_collision.disable_count[part] = frames; + skater.skeleton_collision.parts[part].enabled = false; + } + } +} diff --git a/crates/skate-game/src/physics/player_state/pre_state.rs b/crates/skate-game/src/physics/player_state/pre_state.rs index d2674b71..df0c7175 100644 --- a/crates/skate-game/src/physics/player_state/pre_state.rs +++ b/crates/skate-game/src/physics/player_state/pre_state.rs @@ -15,7 +15,8 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> | PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir - | PhysicalStateId::GroundAnimation + | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant + | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting diff --git a/crates/skate-game/src/physics/player_state/publication.rs b/crates/skate-game/src/physics/player_state/publication.rs index e8185bd1..ecd0a2e5 100644 --- a/crates/skate-game/src/physics/player_state/publication.rs +++ b/crates/skate-game/src/physics/player_state/publication.rs @@ -16,7 +16,8 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> | PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir - | PhysicalStateId::GroundAnimation + | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant + | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting @@ -61,6 +62,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> set(78, skater.wipeout.requests_runout(p)); set(71, player.flags_1296 & (1 << 24) != 0); set(73, p.flags_2476 & (1 << 24) != 0); + set(74, state == PhysicalStateId::HandPlant && skater.handplant.continuation); set(75, state.category() == 500); set(76, state as u32 == 500); //82DB78A8..78DC: the processed off-board request refreshes three @@ -75,6 +77,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> set(77, dismount_requested); set(79, p.state_variant_index_2528 == 3); set(80, p.flags_2484 & (1 << 21) != 0); + set(83, skater.footplant.perform || skater.footplant.flag_627); if let Some(output) = &output { set(84, output.state_28.flag_84); set(86, output.state_28.has_world_grab_intent_without_object); @@ -87,6 +90,11 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> set(84, skater.slide_state.state.wall_riding); } let physical = &mut skater.player_input.physical; + physical.component_1832_word_1876=skater.handplant.flags; + physical.air.handplant_position_304=skater.handplant.anchor.map(f32::to_bits); + physical.air.handplant_time_320=skater.handplant.phase; + physical.air.flag_446=u8::from(skater.handplant.flags&0x8000_0000!=0); + skater.footplant.publish(&mut physical.air); if physics.grind.active { physical.grinds.words_136_140 = [physics.grind.kind, 1]; physical.grinds.flag_318 = u8::from(physics.grind.front_contact); @@ -120,6 +128,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> physical.state = CurrentStateFields { category_12: state.category(), state_16: state as u32, + flag_74: u8::from(state==PhysicalStateId::HandPlant && skater.handplant.continuation), //ProcessOutput82DB7104..7128: selector57 requests State69 here. //The next input/selection enters702; never reset inside selection. flag_69: u8::from(skater.player_state.selector.request_teleport), diff --git a/crates/skate-game/src/physics/player_state/transition.rs b/crates/skate-game/src/physics/player_state/transition.rs index 56d73399..cb362c70 100644 --- a/crates/skate-game/src/physics/player_state/transition.rs +++ b/crates/skate-game/src/physics/player_state/transition.rs @@ -18,6 +18,7 @@ impl PhysicalStateCalls for Calls { | PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir + | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround | PhysicalStateId::WipeoutGround @@ -34,6 +35,7 @@ impl PhysicalStateCalls for Calls { | PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir + | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround | PhysicalStateId::WipeoutGround @@ -62,7 +64,8 @@ pub(super) fn set( | PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir - | PhysicalStateId::GroundAnimation + | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant + | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting @@ -74,6 +77,7 @@ pub(super) fn set( | PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir + | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround | PhysicalStateId::WipeoutGround @@ -157,6 +161,9 @@ pub(super) fn set( //Native82DB8540 publishes Processed state/history BEFORE old Exit. The //same retained objects then receive Exit followed by the new Enter. match current { + PhysicalStateId::HandPlant => skater.handplant.reset(), + PhysicalStateId::FootPlant => skater.footplant.reset(), //Exit82D4C5A8 + PhysicalStateId::Boneless => {}, //empty82D4C9B4 PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific => super::super::grind::exit(physics, skater)?, PhysicalStateId::BipedAir => super::super::offboard::air_state::exit(physics, skater), PhysicalStateId::BipedGround => super::super::offboard::ground_state::exit(physics, skater), @@ -172,6 +179,9 @@ pub(super) fn set( _ => unreachable!("state support checked before publication"), } match requested { + PhysicalStateId::HandPlant => super::super::handplant::enter(physics, skater), + PhysicalStateId::FootPlant => super::super::footplant::ground::enter(physics, skater), + PhysicalStateId::Boneless => super::super::boneless::enter(physics, skater), PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific => super::super::grind::enter(physics, skater), PhysicalStateId::BipedAir => super::super::offboard::air_state::enter(physics, skater), PhysicalStateId::BipedGround => { diff --git a/crates/skate-game/src/physics/skater.rs b/crates/skate-game/src/physics/skater.rs index 532ac365..7a9568cb 100644 --- a/crates/skate-game/src/physics/skater.rs +++ b/crates/skate-game/src/physics/skater.rs @@ -45,6 +45,8 @@ pub(crate) struct SkaterRuntime { pub slide_state: super::slide_state::SlideState, pub trajectory: super::air_trajectory::AirTrajectoryRuntime, pub footplant: super::footplant::Footplant, + pub boneless: super::boneless::Boneless, + pub handplant: super::handplant::Handplant, pub wipeout: super::wipeout::Wipeout, pub wipeout_state: super::wipeout_states::WipeoutState, pub teleport_state: super::teleport_state::Runtime, @@ -206,6 +208,8 @@ impl SkaterRuntime { slide_state: super::slide_state::SlideState::load(&data)?, trajectory: super::air_trajectory::AirTrajectoryRuntime::load(&data)?, footplant: super::footplant::Footplant::load(&data)?, + boneless: super::boneless::Boneless::load(&data)?, + handplant: super::handplant::Handplant::load(&data)?, wipeout: super::wipeout::Wipeout::load(&data)?, wipeout_state, teleport_state: super::teleport_state::Runtime::new(super::teleport_state::Checkpoint { diff --git a/crates/skate-game/src/physics/wipeout.rs b/crates/skate-game/src/physics/wipeout.rs index f935be96..72b6a21b 100644 --- a/crates/skate-game/src/physics/wipeout.rs +++ b/crates/skate-game/src/physics/wipeout.rs @@ -94,6 +94,13 @@ pub(super) fn check_after_physics( grind_normal: skater.trajectory.selector.grind_normal(), }; match skater.player_state.current() { + PhysicalStateId::FootPlant | PhysicalStateId::HandPlant => { + wipeout::check_plant(&mut skater.wipeout.state, &skater.wipeout.settings, &observations.frame()?); + if skater.player_state.current() == PhysicalStateId::FootPlant { + super::footplant::ground::post_physics(skater); + } + Ok(()) + } PhysicalStateId::BipedAir => { let mode = skater.wipeout.mode(&skater.player_input.processed)?; let frame = observations.frame()?; diff --git a/docs/plants.md b/docs/plants.md new file mode 100644 index 00000000..e6c725ea --- /dev/null +++ b/docs/plants.md @@ -0,0 +1,110 @@ +# No-complys, footplants and handplants + +The stock graphs now reach physical states 602 (no-comply/boneless/fastplant), +601 (air footplant) and 600 (handplant). The implementation connects the TU3 +contact selectors, authored animation events, VLT curves, board drives, skeleton +roots, limb IK, release trajectories and state publication. `IsFootPlanting` is +registered in both graphs so its formerly unsupported leaf no longer pauses the +one-foot path. + +## Controller controls + +These are the existing Xbox mappings and stock graph transitions. Stance and +contact determine which animation and physical state can enter. + +| Action | Input | +| --- | --- | +| No-comply | Begin a front-foot push (X in regular, A in goofy), then flick the right stick for an ollie during push entry. The graph accepts the Square gesture group. | +| Boneless / fastplant | Hold one grab trigger fully, then press A or X. The chosen trigger, push foot and stance select the stock variant. | +| Air footplant | Ollie, hold LT or RT for a grab, then press and hold A or X for a one-foot air while approaching a reachable surface. Contact prediction and the extended foot determine entry. | +| Handplant | Hold RB while riding up a steep transition toward reachable coping. Move the right stick to tweak; A/X select one-foot variants and B selects the no-foot variant. | + +No-comply launches come from the clip's `FootJump` event. Footplants release at +the manager's calculated curve duration. Handplant entry requires upward travel, +the authored minimum speed/slope, a selected edge and a valid surface +investigation. Holding RB on flat ground does not force a handplant. + +## Local build + +Run `LaunchPlants.bat` in the repository root. It uses +`bin/plants/skate3rust.exe`, the absolute installed asset directory, and +`--test-world`. Additional command-line arguments are forwarded. The default +test course is useful for no-complys and reachable footplant surfaces; handplants +need transition/coping geometry satisfying the native selector. An installed +converted map can be chosen in the map menu or supplied with `--map`. + +The task executable is a separate copy, built with: + +```powershell +$env:CARGO_TARGET_DIR = 'C:/Users/Daddy/Documents/skate3-imported/target' +$env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = '-C target-feature=+crt-static' +cargo build --release --locked --target x86_64-pc-windows-msvc -p skate-game --no-default-features +``` + +The shared target is retained. Copy its `x86_64-pc-windows-msvc/release/skate3rust.exe` +to `bin/plants/skate3rust.exe` after rebuilding this branch. No installed or +running desktop executable is replaced. The launcher contains a machine-local +asset path; other installations must update that path. + +If another worktree has populated this shared target with different workspace +crates, refresh the modification times of `crates/skate-core/src/lib.rs`, +`crates/skate-data/src/lib.rs` and `crates/skate-game/src/main.rs` before building. +That forces the local crates to rebuild without deleting the dependency cache. + +## Native provenance + +Evidence is the owned TU3 executable image `default_82000000_011B0000.bin`, base +`82000000`, SHA256 +`f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4`, generated PPC +instruction listings, stock OnBoard graphs, OnBoard.abin, and skaterschema/VLT. +Registration strings and vtables identify the graph leaves. VMX128 instruction +operands take precedence over unreliable decompiler output and community names. + +| Behavior | TU3 evidence | +| --- | --- | +| Input | `8259A554/8259AF60` emits GrabWorld from packed bit28; `8259BB18` emits five handplant controls, named by constructors `82F84F28..82F84F98`. Existing A/X pushes feed the authored no-comply and one-foot paths. | +| No-comply / boneless | State602 vtable `82327434`; entry `82D4C900`, update `82D4C9B8`, launch `82D4CB18/82D4CC18`. Anchors the completed physical toe, uses the stock linear/angular drive modes and four launch curves. | +| Footplant | State601 vtable `82327400`; manager Start `82D704B0`, adjustment `82D70CE0`, update `82D70E40`, publication `82D71040`, nearby-edge refinement `82D6FBB0`. Foot/toe volume setup `82D91330`; postphysics `82D4C6D8`. | +| Handplant contact | Manager `82D61040`; ground query `82D38430`, admission `82D61D88`, position selector `82D63D40`, frame/window/edge clipping `82D640F0..82D65D58`. Uses actual world grind primitives, capped at 40 broadphase candidates. | +| Handplant motion | Launch `82D613B0`; six outgoing queries `82D66318`, selection `82D66948`; arc constructors `82D60770/82D608A0`; COM `82D625C0`, Bezier `82D62408`, orientation `82D62C88`, time warp `82D631A8/82D63280`. Collision position accessor `82D2D988` and landing-normal accessor `82D2D9E8`. | +| Handplant skeleton | State600 vtable `823273CC`; entry/update `82D4C350/82D4C3D8`; reckoning `82D8E1C0`; hand IK `82D633B0`, target clamp `82BD9728/82BD97D0`, upper-body volume suppression `82D91298`. | +| Shared skeleton | Anchored-foot path `82BDEC48`, COM path `82BDF090`, root positioning `82BDEEB8/82BE0E80`. Uses the existing completed animation, physical skeleton, board and four-limb IK owners. | +| Graph timing | `IsFootPlanting` `82BBD888`; preparation `82BBD7F0`; FootPlantAbsorb Update `82BBE4E0`; handplant phase/new-position `82BBDA98/82BBDB98`; antic Begin `82BBE5D0`; handplant scoring Update `82BBF758`. | +| Lifecycle | Footplant release `82D8B970` reads big-endian byte2480 bit0, hence full-word bit24. Common handplant publication `82DB7130..82DB7190`; shared plant wipeout check `82D8FDC0`. Teleport/reset paths clear active plant state. | + +Settings are loaded from the installation, using the schema's cached layout: + +| Collection class | Used data | +| --- | --- | +| `CCB95A83C78B4FF9/default` | Four PointNegGraphData8 launch curves for state602. | +| `physics_footplantmanager/default` (`172D553937D8E07C`) | Radial speed curve; contact/deck dimensions; leg lengths; Bezier handles; minimum/maximum plant duration; release and wipeout limits. | +| `55B5940075F78842/default` | Seven PointGraph4 query-window curves; depth and vertical drop. | +| `physics_handplantmanager/default` (`0F38084037CB9175`) | Time warp; orientation/heading curves; hand-radius curve; entry/release/IK timings; admission thresholds; apex and outgoing-velocity parameters. | +| `anim_handplant/default` (`F190522E738EC5CB`) | Out/into/antic thresholds at offsets0/4/8. | + +The antic blend reads actual clip lengths: `INVERT_HANDPLANT_FS_0_ANTIC` is +0.2 seconds and `INVERT_HANDPLANT_FS_0_ANTIC_LATE` is 1/30 second. FootPlantAbsorb +publishes `absorblength` from the physical manager; no host animation timer or +extra jump impulse selects these launches. + +## Validation and boundaries + +Release compilation succeeded. Static inspection checked all 32 operation names +in the seven plant templates against source registrations, the five stock +collection classes and point-graph layouts, both antic clip lengths, and the +`FootJump` attributes in the no-comply/boneless/fastplant clips. This is not a +runtime graph-coverage or gameplay-parity result. + +The game, launcher, recomp and gameplay harnesses were not executed. In-game +entry/release, contacts, stance combinations, wipeouts, visuals and preservation +of skating/off-board/grind behavior remain for the user's validation. + +Queries use the engine's existing synchronous static-world collision and grind +geometry adapters. The outgoing batch is consumed on handplant entry rather +than using TU3's asynchronous job-completion gate and redundant ground-probe +completion dependency. Moving-object coping is not supplied by this adapter. +Scalar vector/trigonometric helpers and quaternion-derived axis-angle rotation +reuse existing engine math; bit-exact Xenon/VMX execution is not claimed. +Handplant scoring updates the existing internal score packet, whose presentation +remains outside this change. Stock assets and private research/build files are +excluded from the commit.