mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Implement TU3 no-comply, footplant and handplant physics
This commit is contained in:
1 parent
da364c924f
commit
55cc1eff82
40 files changed
+1829
-28
No files matched your search
@@ -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%
|
||||
@@ -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;
|
||||
@@ -81,6 +81,24 @@ pub fn produce(controller: &DerivedControllerInput) -> Vec<TrickIntent> {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<skate_core::animation::output::attributes::AttributeName>,
|
||||
pub name: Option<u32>,
|
||||
|
||||
@@ -149,6 +149,9 @@ impl OperationFactory for MotionFactory {
|
||||
)),
|
||||
"ClearTrickAttr" => Some(MotionOperation::ClearTrickAttr),
|
||||
"SetTrickHeight"
|
||||
| "FootPlantAbsorb"
|
||||
| "SetHandPlantAnticLength"
|
||||
| "ScoringHandPlants"
|
||||
| "SetTrickAttr"
|
||||
| "ScoringTrick"
|
||||
| "MonitorUnderflip"
|
||||
|
||||
@@ -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<String>;4], intents: [String;2] },
|
||||
Height {
|
||||
from: AttributeName,
|
||||
rename: AttributeName,
|
||||
@@ -26,6 +29,13 @@ pub enum Operation {
|
||||
impl Operation {
|
||||
pub fn parse(a: &Attributes<'_>) -> Option<Self> {
|
||||
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<f32,String> {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Self, String> {
|
||||
let (settings, modes) = settings::load(data)?;
|
||||
Ok(Self {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Self, String> {
|
||||
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(())
|
||||
}
|
||||
@@ -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<Self, String> {
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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<Self, String> {
|
||||
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")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
@@ -164,6 +164,7 @@ pub(crate) fn advance(
|
||||
physics: &mut GamePhysics,
|
||||
skater: &mut SkaterRuntime,
|
||||
) -> Result<GroundBoardOutcome, String> {
|
||||
super::handplant::ground_query(physics,skater);
|
||||
let p = &skater.player_input.processed;
|
||||
let toolkit = skater
|
||||
.player_input
|
||||
|
||||
@@ -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<contact::Candidate>,
|
||||
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<Self, String> {
|
||||
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::<f32>()
|
||||
* 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)
|
||||
}
|
||||
@@ -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<Candidate> {
|
||||
//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<localZ<0.
|
||||
if !clip_depth(&mut segment, -settings.depth, true) || !clip_depth(&mut segment, 0.0, false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut valid = true;
|
||||
for i in 0..4 {
|
||||
let delta = sub(polygon[(i + 1) % 4], polygon[i]);
|
||||
let n = normalize([delta[1], -delta[0], 0.0, 0.0]);
|
||||
let d0 = dot(sub(segment[0], polygon[i]), n);
|
||||
let d1 = dot(sub(segment[1], polygon[i]), n);
|
||||
if !clip(&mut segment, d0, d1) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
candidates.push((*edge, segment));
|
||||
}
|
||||
}
|
||||
let mut best = None;
|
||||
let mut height = -f32::MAX;
|
||||
let mut depth = -f32::MAX;
|
||||
for (i, (_, segment)) in candidates.iter().enumerate() {
|
||||
let p = if segment[0][1] > 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<Self, String> {
|
||||
let root = "Hash_55B5940075F78842";
|
||||
let graph4 = |class, name| -> Result<PointGraph<4>, 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<PointGraph<8>, 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")?,
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<usize>,
|
||||
) -> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
+110
@@ -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.
|
||||
Reference in new issue
Block a user