Files
skate-3-rust-engine/crates/skate-game/src/physics/grind.rs
T

855 lines
32 KiB
Rust

//! Live straight-spline grind owner. TU3 truck query82C1FDC0, paired scorer
//!82D89150, 50-50 update82D41D70 and exit82D3F430.
use super::{GamePhysics, SkaterRuntime};
use skate_core::riding::ground_correction_math::dot_product as dot3;
use skate_core::{
animation::skeleton_input::name::encode,
math::{Basis3, Vector3},
physics::{
drive_frames::RetailAffineTransform,
filtered_state::GrindState,
force_queue::QueuedPointForce,
grind_contact::{self, FiftyFiftyCandidate, Primitive},
grind_forces,
},
player::state::PhysicalStateId,
point_graph::PointGraph,
};
use skate_data::collections::Collections;
type V = [f32; 4];
#[path = "grind_camera.rs"]
mod camera;
#[path = "grind_chromosome.rs"]
mod chromosome;
pub(crate) struct Runtime {
camera: camera::GrindCamera,
pub(super) primitives: Vec<Primitive>,
pub candidate: Option<FiftyFiftyCandidate>,
pub name: String,
pub active: bool,
pub kind: u32,
pub front_contact: bool,
chromosome: chromosome::Chromosome,
scoring_name: String,
ground_frames: u32,
crouch: f32,
control: grind_contact::control::Control,
test_depth_epsilon: f32,
test_depth: f32,
exiting: bool,
release: grind_forces::Release,
surface_kind: u32,
surface_normal: V,
surface_side: V,
entry_velocity: Option<V>,
maximum_entry_delta: f32,
truck_to_wheel: f32,
deck_to_truck: f32,
pin_vs_slope: PointGraph<4>,
pub distance: f32,
pop_heights: [[f32; 2]; 5],
boardslide_pop_heights: [[f32; 2]; 5],
tipslide_pop_heights: [[f32; 2]; 5],
manager_age: f32,
pub launched: bool,
pub launch_velocity: V,
cooldown: u32,
previous_position: V,
diagnostic_tick: u32,
}
impl Runtime {
pub fn load(
data: &Collections,
map: Option<&skate_data::skate_map::SkateMap>,
) -> Result<Self, String> {
let graph = data
.words::<8>("physics_grinds", "default", "PinVsSlope")?
.map(f32::from_bits);
Ok(Self {
camera: camera::GrindCamera::default(),
primitives: crate::grind_world::primitives(map)?,
candidate: None,
name: String::new(),
active: false,
kind: 0,
front_contact: false,
chromosome: chromosome::Chromosome::default(),
scoring_name: String::new(),
ground_frames: 0,
crouch: 0.0,
control: grind_contact::control::Control::default(),
test_depth_epsilon: data.float("physics_grinds", "default", "TestDepthEpsilon")?,
test_depth: data.float("physics_grinds", "default", "TestDepth")?,
exiting: false,
release: grind_forces::Release::default(),
surface_kind: 0,
surface_normal: [0.0, 1.0, 0.0, 0.0],
surface_side: [0.0; 4],
entry_velocity: None,
maximum_entry_delta: data.float("physics_wipeout", "default", "Wipeout_AirMaxSpeedIntoCollisionNearGrind")?,
truck_to_wheel: data.float("physics_grinds", "default", "TruckToWheel")?,
deck_to_truck: data.float("physics_grinds", "default", "DeckCenterToTruck")?,
pin_vs_slope: PointGraph {
x: graph[..4].try_into().unwrap(),
y: graph[4..8].try_into().unwrap(),
},
pop_heights: ["easy", "normal", "hardcore", "motorized", "test"]
.map(|key| {
Ok::<_, String>([
data.float("physics_mode", key, "Hash_3097A69281990652")?,
data.float("physics_mode", key, "Hash_FA4CDBAE0DFD1FAD")?,
])
})
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.try_into()
.unwrap(),
//82D41CE8 consumes mode100/104;50-50's82D40D60 uses92/96.
boardslide_pop_heights: ["easy", "normal", "hardcore", "motorized", "test"]
.map(|key| {
Ok::<_, String>([
data.float("physics_mode", key, "Hash_B2B1170AFFC8AC69")?,
data.float("physics_mode", key, "Hash_1B3E9F9C836D287D")?,
])
})
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.try_into()
.unwrap(),
tipslide_pop_heights: ["easy", "normal", "hardcore", "motorized", "test"]
.map(|key| {
Ok::<_, String>([
data.float("physics_mode", key, "Hash_703829BD711E54DE")?,
data.float("physics_mode", key, "Hash_F2473E9125079F0")?,
])
})
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.try_into()
.unwrap(),
manager_age: 0.0,
launched: false,
launch_velocity: [0.; 4],
cooldown: 0,
distance: 0.0,
previous_position: [0.; 4],
diagnostic_tick: 0,
})
}
pub fn advance_camera(&mut self, velocity: V) -> Result<(), String> {
if self.active {
let contact = self.candidate.ok_or("Grind camera needs active contact")?;
self.camera.update(
contact,
self.primitives[contact.primitive],
self.kind,
velocity,
);
} else {
self.camera.exit();
}
Ok(())
}
pub fn camera_output(&self) -> crate::camera::CameraGrindOutput {
crate::camera::CameraGrindOutput {
//Reset82DE3518 defaults still apply outside the active state.
direction_0: if self.active {
self.camera.direction
} else {
[1.0, 0.0, 0.0, 0.0]
},
camera_target_96: if self.active {
self.camera.target
} else {
[0.0; 4]
},
grinding_316: u8::from(self.active),
}
}
pub fn output(&self) -> GrindState {
if !self.active {
return GrindState::default();
}
let owner = self
.candidate
.map(|c| self.primitives[c.primitive].owner)
.unwrap_or(u64::MAX);
GrindState {
kind: self.kind as i32,
name: encode(self.name.as_bytes()),
scoring_name: encode(self.scoring_name.as_bytes()),
on_front: self.front_contact,
crouch: self.crouch,
pathed_guid: owner,
local_guid: owner,
..GrindState::default()
}
}
}
///Run after ProcessInput, before the native state selector. Geometry is queried
///against both current trucks; merely being near a rail cannot acquire it.
pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
let p = &mut skater.player_input.processed;
let runtime = &mut physics.grind;
runtime.entry_velocity = None;
let board = super::solve::deck_frame(&physics.board);
let mut effective_board = board;
if p.flags_2468 & 0x0010_0000 != 0 {
effective_board[0] = effective_board[0].map(|v| -v);
effective_board[2] = effective_board[2].map(|v| -v);
}
let pose = chromosome::Pose {
animated_board: skater.animated_skeleton.board_frames.animation_target,
board: effective_board,
foot_directions: [
skater.skeleton.record.pose[19][0],
skater.skeleton.record.pose[15][0],
],
fakie: skater.animation.packet.riding_fakie,
};
runtime.chromosome.observe(pose, p.category_2512);
runtime.cooldown = runtime.cooldown.saturating_sub(1);
runtime.manager_age = (runtime.manager_age + 0.0035).min(1.0);
let old = runtime.candidate;
runtime.candidate = None;
runtime.diagnostic_tick = runtime.diagnostic_tick.wrapping_add(1);
runtime.ground_frames = if p.category_2512 == 100 {
runtime.ground_frames.saturating_add(1)
} else {
0
};
let gated = runtime.cooldown > 0
|| skater.player_input.grind.disabled
|| p.flags_2476 & 0x01000000 != 0;
// Native GrindData query82D8A828: a 1.2m half-extent, at most40 chords.
let nearby: Vec<usize> = runtime
.primitives
.iter()
.enumerate()
.filter(|(_, edge)| {
(0..3).all(|i| {
edge.start[i].min(edge.end[i]) <= board[3][i] + 1.2
&& edge.start[i].max(edge.end[i]) >= board[3][i] - 1.2
})
})
.map(|(i, _)| i)
.take(40)
.collect();
if nearby.is_empty() {
return;
}
let edges: Vec<Primitive> = nearby.iter().map(|i| runtime.primitives[*i]).collect();
let mut hits = grind_contact::truck_contacts(
board,
p.flags_2484,
runtime.truck_to_wheel,
runtime.deck_to_truck,
&edges,
);
for hit in hits.iter_mut().flatten() {
hit.primitive = nearby[hit.primitive];
}
let extra = &skater.animation_input.extra;
let fifty = grind_contact::fifty_fifty_candidate_on_splines(
board,
[extra.grind_translation, extra.grind_stability_nudge],
hits,
&runtime.primitives,
);
let velocity = p.vectors_400_416[0].map(f32::from_bits);
let slide = grind_contact::deck_contact(
board,
p.flags_2484,
runtime.test_depth_epsilon,
runtime.test_depth,
runtime.deck_to_truck,
&edges,
)
.and_then(|mut hit| {
hit.primitive = nearby[hit.primitive];
grind_contact::boardslide_candidate(
board,
hit,
runtime.primitives[hit.primitive],
velocity,
p.category_2512,
skater.player_input.grind.low_wheel_frames as i32,
p.flags_2476,
runtime.deck_to_truck,
runtime.truck_to_wheel,
)
});
use grind_contact::families::{self, Contact};
let five = families::five_o(
board,
hits,
&runtime.primitives,
velocity,
p.flags_2476,
p.flags_2472,
runtime.ground_frames,
extra.grind_translation,
);
let mut tip_hits = families::tip_contacts(
board,
p.flags_2484,
p.state_2504,
p.flags_2468,
runtime.deck_to_truck,
&edges,
);
for hit in tip_hits.iter_mut().flatten() {
hit.primitive = nearby[hit.primitive];
}
let tip = families::tipslide(
board,
tip_hits,
&runtime.primitives,
velocity,
p.category_2512,
p.flags_2476,
p.flags_2472,
runtime.ground_frames,
skater.animation_input.fields.balance,
p.vectors_464_480_496_512_528[0].map(f32::from_bits),
);
let dark = families::inverted_contact(
board,
p.flags_2484,
runtime.test_depth_epsilon,
runtime.test_depth,
&edges,
)
.and_then(|mut hit| {
hit.primitive = nearby[hit.primitive];
families::darkslide(
board,
hit,
runtime.primitives[hit.primitive],
velocity,
p.category_2512,
skater.player_input.grind.low_wheel_frames,
)
});
let fifty = fifty.map(|geometry| Contact {
geometry,
front: false,
kind: 0,
});
let slide = slide.map(|geometry| Contact {
geometry,
front: false,
kind: 1,
});
//82D875A8 retains a valid current family, then truck/tip/deck/inverted.
let current = if runtime.active {
match runtime.kind {
0 | 3 => fifty.or(five),
1 => slide,
2 | 4 => tip,
5 => dark,
_ => None,
}
} else {
None
};
let selected = current.or(fifty).or(five).or(tip).or(slide).or(dark);
let candidate = selected.map(|c| c.geometry);
let mut kind = selected.map_or(0, |c| c.kind);
runtime.front_contact = selected.is_some_and(|c| c.front);
// Passive diagnostics for the user's visual run; no input simulation.
if runtime.diagnostic_tick % 30 == 0 || (!gated && candidate.is_some()) != old.is_some() {
bevy::log::info!(
"GRIND_QUERY state={} gated={} nearby={} hits={:?} depth={:?} candidate={} board={:?} up={:?} forward={:?} probe={:?} flags={:08x}/{:08x}/{:08x}/{:08x}/{:08x}",
p.state_2508,
gated,
nearby.len(),
hits.map(|h| h.map(|h| h.primitive)),
hits.map(|h| h.map(|h| dot3(sub(board[3], h.position), board[1]))),
candidate.is_some(),
board[3],
board[1],
board[2],
[runtime.truck_to_wheel, runtime.deck_to_truck],
p.flags_2468,
p.flags_2472,
p.flags_2476,
p.flags_2480,
p.flags_2484
);
}
if gated {
return;
}
let Some(mut candidate) = candidate else {
return;
};
let velocity = p.vectors_400_416[0].map(f32::from_bits);
//82D886B8: air accepts an actual intersection; same active grind uses90deg.
//The ordinary level-ground admission uses17deg.
let angle = if p.category_2512 == 400 || p.state_2508 == 701 {
90.0
} else {
17.0
};
if p.category_2512 != 200
&& !grind_contact::within_approach_angle(candidate.direction, velocity, angle)
{
return;
}
runtime.candidate = Some(candidate);
runtime.kind = kind;
{
let edge = runtime.primitives[candidate.primitive];
match skate_core::air::trajectory::grind_surface::investigate(
&physics.world,
edge.start,
edge.end,
candidate.centre,
runtime.deck_to_truck,
) {
Ok(Some(surface)) => {
if surface.evidence.kind == 3 {
runtime.candidate = None;
return;
}
runtime.surface_kind = surface.evidence.kind;
runtime.surface_normal = surface.normal;
runtime.surface_side = surface.evidence.side;
if kind == 2
&& families::is_backslash(
board[3],
candidate.centre,
surface.far_points,
surface.normal,
runtime.deck_to_truck,
p.state_2508,
)
{
kind = 4;
runtime.kind = kind;
}
}
Ok(None) | Err(_) => {
runtime.candidate = None;
return;
}
}
}
//82D87460: primitive tangent follows travel, retaining its previous sign at rest.
let edge = runtime.primitives[candidate.primitive];
let delta = sub(edge.end, edge.start);
let length = dot3(delta, delta).sqrt();
let mut direction = delta.map(|v| v / length);
let along = dot3(direction, velocity);
if along < 0.0 {
direction = direction.map(|v| -v);
}
if along.abs() < 0.1 && dot3(p.grind_direction_1136.map(f32::from_bits), direction) < -0.9 {
direction = direction.map(|v| -v);
}
candidate.direction = direction;
runtime.candidate = Some(candidate);
p.grind_position_1120 = candidate.centre.map(f32::to_bits);
p.grind_direction_1136 = direction.map(f32::to_bits);
let normal = if kind == 0 {
grind_contact::upright_normal(direction)
} else {
runtime.surface_normal
};
//82D86F00 uses Processed608, the retained airborne trajectory velocity,
//not the board's post-contact400. Consume once after state selection.
if p.category_2512 == 200 {
let air_velocity = p.vectors_544_560_592_608[3].map(f32::from_bits);
let corrected = grind_contact::entry::airborne_velocity(kind, direction, normal, air_velocity);
let rejected = grind_contact::entry::airborne_rejections(
direction, p.vectors_544_560_592_608[0].map(f32::from_bits), velocity,
air_velocity, corrected, runtime.surface_kind, runtime.surface_side,
runtime.maximum_entry_delta,
);
if !rejected.is_empty() {
runtime.candidate = None;
p.flags_2468 |= 0x0004_0000;
for reason in rejected { skater.wipeout.state.request(reason, 0.0); }
return;
}
runtime.entry_velocity = Some(corrected);
}
let (name, scoring_name) =
runtime
.chromosome
.update(pose, kind, candidate.centre, direction, normal);
runtime.name = name.into();
runtime.scoring_name = scoring_name.into();
}
pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
physics.grind.active = matches!(
skater.player_state.current(),
PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::GrindBoardslide
| PhysicalStateId::GrindTipslide
| PhysicalStateId::GrindFiveO
| PhysicalStateId::GrindBackslash
| PhysicalStateId::GrindDarkslide
);
physics.grind.exiting = false;
physics.grind.release = grind_forces::Release::default();
physics.grind.distance = 0.0;
physics.grind.previous_position = super::solve::deck_frame(&physics.board)[3];
skater.ground_lifecycle.skeleton_elapsed_16505 = true;
physics
.board
.hook_mut()
.drive
.disable_animation(&mut skater.ground_lifecycle.board_animated_290);
skater.air_reckoning.state.reset_spin();
if physics.grind.active {
bevy::log::info!(
"Grind entered: {} rail={:?}",
physics.grind.name,
physics
.grind
.candidate
.map(|c| physics.grind.primitives[c.primitive].owner)
);
}
Ok(())
}
pub(super) fn exit(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
physics.grind.active = false;
physics
.board
.hook_mut()
.drive
.disable_animation(&mut skater.ground_lifecycle.board_animated_290);
//82D3F7D0 restores all four wheel drags to zero.
for body in &mut physics.board.bodies_mut()[..4] {
body.inertia.linear_drag = 0.0;
}
Ok(())
}
pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
physics.grind.launched = false;
let board = super::solve::deck_frame(&physics.board);
let p = &skater.player_input.processed;
if physics.grind.active {
//82D40D90; Boardslide82D41788 only exposes this exit on kind2.
let explicit = p.flags_2476 & 0x2000_0000 != 0
&& (!matches!(physics.grind.kind, 1 | 5) || physics.grind.surface_kind == 2);
let was_leaving = physics.grind.release.leaving;
physics.grind.release.begin_update(p.scalar_2652, explicit);
if !was_leaving && physics.grind.release.leaving {
bevy::log::info!("GRIND_RELEASE kind={} speed={} explicit={}", physics.grind.kind, p.scalar_2652, explicit);
}
if physics.grind.release.finish_update() {
//82D3F638: byte37/value124, using the retained request owner.
skater.wipeout.state.request(17, 0.0);
}
}
if physics.grind.active && p.flags_2468 & 0x00400000 != 0 {
let c = physics
.grind
.candidate
.ok_or("Grind pop has no primitive")?;
let heights = if physics.grind.kind == 1 {
&physics.grind.boardslide_pop_heights
} else if physics.grind.kind == 2 {
&physics.grind.tipslide_pop_heights
} else {
&physics.grind.pop_heights
};
let [min, max] = heights[p.state_variant_index_2528 as usize];
let strength = skater.animation_input.extra.jump_strength;
let velocity = grind_forces::fifty_fifty_pop(
p.vectors_400_416[0].map(f32::from_bits),
grind_contact::upright_normal(c.direction),
c.direction,
board[3],
c.centre,
(1.0 - strength).mul_add(min, max * strength),
skater.animation_input.extra.grind_stability_nudge,
physics.grind.manager_age,
);
physics.grind.launched = true;
physics.grind.launch_velocity = velocity;
physics.grind.cooldown = 10;
physics.grind.manager_age = (physics.grind.manager_age - 0.2).max(0.0);
skater
.ground_runtime
.set_animated_velocity(&mut physics.board, velocity);
physics
.board
.hook_mut()
.drive
.disable_animation(&mut skater.ground_lifecycle.board_animated_290);
return super::input_phase::update_grind_jump(physics, skater);
}
if physics.grind.active {
if let Some(velocity) = physics.grind.entry_velocity.take() {
//82D40AF8 ->82C04168: all seven bodies, preserving angular rates.
bevy::log::info!("GRIND_ENTRY board_velocity={:?} airborne_velocity={:?} applied_velocity={:?}",
p.vectors_400_416[0].map(f32::from_bits),
p.vectors_544_560_592_608[3].map(f32::from_bits), velocity);
skater.ground_runtime.set_animated_velocity(&mut physics.board, velocity);
//82D40CF8 also supplies Skeleton16112/16416 for this same tick.
skater.animated_skeleton.roots.supplied_prediction = Some(
core::array::from_fn(|i| velocity[i].mul_add(p.timestep_2604, board[3][i])));
//The native entry-velocity branch bypasses the ordinary force
//update on this tick; its Ground skeleton continuation still runs.
let contact = physics.grind.candidate.ok_or("Grind entry lost its contact")?;
let normal = if physics.grind.kind == 0 {
grind_contact::upright_normal(contact.direction)
} else { physics.grind.surface_normal };
skater.ground.steering.targets = [0.0; 2];
physics.riding.update_grind_reckoning(
&mut skater.air_reckoning.state, normal, board[2], p.flags_2468, 0.9);
return super::input_phase::update_ground(physics, skater);
}
}
let velocity = p.vectors_400_416[0].map(f32::from_bits);
//Both state vtables neutralize the trucks before the shared board solve.
skater.ground.steering.targets = [0.; 2];
let normal = if physics.grind.active {
let c = physics
.grind
.candidate
.ok_or("Active grind lost its candidate before selection")?;
let normal = if physics.grind.kind != 0 {
physics.grind.surface_normal
} else {
grind_contact::upright_normal(c.direction)
};
physics.grind.crouch = dot3(board[2], normal).abs() * 0.4;
if physics.grind.release.leaving {
let surface = physics.grind.surface_kind;
let (strength, limit, lift, force_across) = match physics.grind.kind {
//50-50 exit82D41DF0, 5-O exit82D428C0.
0 => if surface < 2 { (100.0, 1.0, 125.0, false) } else { (10.0, 1.0, 0.0, false) },
3 => if surface < 2 { (120.0, 0.9, 125.0, false) } else { (10.0, 0.9, 0.0, false) },
//Static rail Tipslide82D420A0 and Backslash82D42A80.
2 => (100.0, 1.5, 0.0, true),
4 => (100.0, 0.75, if surface == 1 { 125.0 } else { 0.0 }, surface == 1),
//Boardslide82D41250 enters the existing runout/wipeout path
//on rail/ledge engagement; kind2 uses a physical side force.
_ => (90.0, 0.75, 0.0, false),
};
if matches!(physics.grind.kind, 1 | 5) && surface != 2 {
skater.wipeout.state.request(10, 0.0);
} else {
append(physics, grind_forces::release_force(
board[3], c.centre, c.direction, normal, velocity, surface,
physics.grind.surface_side, force_across, strength, limit, lift,
))?;
}
physics.riding.update_grind_reckoning(
&mut skater.air_reckoning.state, normal, board[2], p.flags_2468, 0.9);
return super::input_phase::update_ground(physics, skater);
}
let across = cross(c.direction, normal);
physics.grind.control.update(
physics.grind.kind,
board[2],
normal,
physics.grind.front_contact,
p.flags_2468 & 0x0010_0000 != 0,
false,
skater.animation_input.extra.grind_translation,
skater.animation_input.extra.grind_stability_nudge,
skater.animation_input.extra.grind_up_down,
skater.animation_input.extra.grind_grab_min_height,
);
if matches!(physics.grind.kind, 1 | 5) {
physics.grind.exiting |= p.flags_2468 & 0x0020_0000 != 0;
let mass = skater
.player_input
.toolkit
.as_ref()
.ok_or("Boardslide needs board mass")?
.total_mass;
for force in grind_forces::boardslide_control(
board[3],
c.centre,
c.direction,
normal,
velocity,
//Darkslide82D41438 scales translation40 by remaining .16m
//deck width; Boardslide82D419A0 uses constant25.
skater.animation_input.extra.grind_translation
* if physics.grind.kind == 5 {
(1.0 - dot3(sub(board[3], c.centre), across).abs() * 6.25) * 40.0 / 25.0
} else {
1.0
},
mass,
physics.grind.exiting,
physics.grind.surface_kind == 2,
) {
append(physics, force)?;
}
} else if physics.grind.kind == 2 {
//82D42390: lateral direction points toward the contact. The
//animation nudge changes sign with the contact side.
let offset = sub(board[3], c.centre);
let inward = across.map(|v| {
v * if dot3(across, offset) > 0.0 {
-1.0
} else {
1.0
}
});
let nudge = skater.animation_input.extra.grind_stability_nudge
* if dot3(normal, cross(c.direction, offset)) > 0.0 {
1.0
} else {
-1.0
};
append(physics, inward.map(|v| v * nudge * 37.0))?;
} else {
let (strength, forward_offset, up_offset) = match physics.grind.kind {
3 => (3000.0, 0.234, 0.07),
4 => (3000.0, 0.39, 0.025),
_ => (800.0, 0.0, 0.07),
};
let force = grind_forces::lateral_pin(
board,
c.centre,
across,
velocity,
strength,
forward_offset,
up_offset,
physics.grind.front_contact,
physics.grind.pin_vs_slope.evaluate(dot3(normal, normal)),
);
append(physics, force)?;
if matches!(physics.grind.kind, 0 | 3) {
//82D401F8: support load from the native pitch conditioner.
append(
physics,
normal.map(|v| v * physics.grind.control.pitch.abs() * -600.0),
)?;
}
}
//The primitive support frame is static and level for the authored rails.
//82D86318 publishes this unit support normal in candidate304.
let friction = grind_forces::friction(
velocity,
normal,
normal,
skater.player_input.grind.friction_vs_time,
false,
1.3,
0,
match physics.grind.kind {
1 | 5 => [55., 55., 60.],
2 => [60., 60., 60.],
4 => [70., 80., 80.],
_ => [50., 40., 40.],
},
);
append(physics, friction)?;
let forward = if dot3(board[2], c.direction) > 0.0 {
c.direction
} else {
c.direction.map(|x| -x)
};
let target = if matches!(physics.grind.kind, 1 | 5) {
//82D418B8 aligns the board's RIGHT axis with the rail;50-50
//82D41E38 aligns its FORWARD axis instead.
let right = if dot3(board[0], c.direction) > 0.0 {
c.direction
} else {
c.direction.map(|x| -x)
};
let up = if physics.grind.kind == 5 {
normal.map(|v| -v)
} else {
normal
};
[right, up, cross(right, up), board[3]]
} else if matches!(physics.grind.kind, 2 | 4) {
grind_contact::control::tip_frame(
board,
c.direction,
normal,
c.centre,
physics.grind.kind == 4,
)
} else if physics.grind.kind == 3
|| (physics.grind.kind == 0 && physics.grind.control.pitch.abs() > 0.12)
{
grind_contact::control::truck_frame(
board,
normal,
&physics.grind.control,
p.flags_2468 & 0x0010_0000 != 0,
)
} else {
[cross(normal, forward), normal, forward, board[3]]
};
//82D40890 uses constant0.1 for interpolation; its f1 parameter is
//separate random perturbation amplitude, not the interpolation weight.
let (frame, _) = skate_core::animation::foot_ik::interpolate_native(&board, &target, 0.1);
physics.board.set_hook_transform(RetailAffineTransform {
basis: Basis3 {
columns: std::array::from_fn(|i| [frame[i][0], frame[i][1], frame[i][2]]),
},
translation: xyz(frame[3]),
});
physics
.board
.hook_mut()
.drive
.enable_angular_only(&mut skater.ground_lifecycle.board_animated_290);
physics.grind.distance +=
dot3(sub(board[3], physics.grind.previous_position), c.direction).abs();
physics.grind.previous_position = board[3];
normal
} else {
//Nonspecific82D42F50 applies the native -40 support force while the
//selector waits for actual wheel/collision observations after rail exit.
append(
physics,
p.vectors_544_560_592_608[0]
.map(f32::from_bits)
.map(|v| v * -40.0),
)?;
board[1]
};
physics.riding.update_grind_reckoning(
&mut skater.air_reckoning.state,
normal,
board[2],
skater.player_input.processed.flags_2468,
if physics.grind.active { 0.9 } else { 0.98 },
);
//82D40D40 and the ordinary nonspecific branch both call Skeleton82BDF530.
super::input_phase::update_ground(physics, skater)
}
fn append(physics: &mut GamePhysics, force: V) -> Result<(), String> {
if !physics.board.forces_mut().append(QueuedPointForce {
tag: 0,
force_world: xyz(force),
point_body: Vector3::ZERO,
}) {
return Err("Grind force queue exhausted".into());
}
Ok(())
}
fn xyz(v: V) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
fn cross(a: V, b: V) -> V {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
0.,
]
}