mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Connect centre-deck boardslide contacts and physical state
This commit is contained in:
1 parent
d60a8d7cd3
commit
685d55601e
13 files changed
+824
-187
No files matched your search
@@ -1,82 +1,195 @@
|
||||
//! TU3 rail-trajectory candidate leaves82D60C80/82D60B98,
|
||||
//! 82D6A398 and82D6A168. Admission and physical adjustment are separate owners.
|
||||
use super::{math::*, Prediction, Trajectory};
|
||||
use super::{Prediction, Trajectory, math::*};
|
||||
use crate::{physics::grind_contact::Primitive, point_graph::PointGraph};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GrindTrajectoryCandidate {
|
||||
pub point: Vector, //0 projected point on rail
|
||||
pub trajectory_point: Vector, //16 centre with radius/offset removed
|
||||
pub direction: Vector, //32 normalized rail direction
|
||||
pub approach: Vector, //48 velocity rejected from rail normal
|
||||
pub distance: f32, //64 perpendicular miss distance
|
||||
pub time: f32, //68 crossing time
|
||||
pub angle: f32, //72 folded approach angle
|
||||
pub frame: i32, //76 fixed60Hz time
|
||||
pub primitive: usize, //80 query index
|
||||
pub point: Vector, //0 projected point on rail
|
||||
pub trajectory_point: Vector, //16 centre with radius/offset removed
|
||||
pub direction: Vector, //32 normalized rail direction
|
||||
pub approach: Vector, //48 velocity rejected from rail normal
|
||||
pub distance: f32, //64 perpendicular miss distance
|
||||
pub time: f32, //68 crossing time
|
||||
pub angle: f32, //72 folded approach angle
|
||||
pub frame: i32, //76 fixed60Hz time
|
||||
pub primitive: usize, //80 query index
|
||||
}
|
||||
|
||||
///The82C21930 result must come from the world geometry investigator. A
|
||||
///spline's authored name or a nearest-distance guess cannot supply these lanes.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GrindSurfaceEvidence {
|
||||
pub kind: u32, //0 rail,1 edge,2 ledge,3 rejected
|
||||
pub side: Vector, //classification64
|
||||
pub kind: u32, //0 rail,1 edge,2 ledge,3 rejected
|
||||
pub side: Vector, //classification64
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GrindAssistLimits {
|
||||
pub lock_distance: f32, //physics_mode80
|
||||
pub lock_distance: f32, //physics_mode80
|
||||
pub max_speed_squared_ledge: f32, //physics_trajectory520
|
||||
pub max_speed_squared_rail: f32, //524
|
||||
pub max_downward_speed: f32, //528
|
||||
pub ledge_scalars: [f32;4], //532,536,540,544
|
||||
pub tip_scalar: f32, //512
|
||||
pub maximum_adjust_angle: f32, //560 degrees
|
||||
pub ledge_scalars: [f32; 4], //532,536,540,544
|
||||
pub tip_scalar: f32, //512
|
||||
pub maximum_adjust_angle: f32, //560 degrees
|
||||
///Actual deck dimensions from82762E50 (DeckMidLength) and82C00868.
|
||||
pub deck_dimensions: [f32;2],
|
||||
pub deck_dimensions: [f32; 2],
|
||||
}
|
||||
|
||||
///Accepted-target publication82D6AEA4..82D6AF38. The original contact
|
||||
///position/material stay intact; time, frame, trajectory and normal change.
|
||||
pub fn apply_admitted_target(
|
||||
prediction: &mut Prediction,
|
||||
candidate: GrindTrajectoryCandidate,
|
||||
correction: Vector,
|
||||
support_normal: Vector,
|
||||
reference_velocity: Vector,
|
||||
maximum_adjust: f32,
|
||||
landing_velocity_scalar: f32,
|
||||
landing_max_angle: f32,
|
||||
) {
|
||||
super::launch::adjust_trajectory(
|
||||
&mut prediction.request.trajectory,
|
||||
candidate.frame,
|
||||
correction,
|
||||
maximum_adjust,
|
||||
);
|
||||
prediction.result.contact_frame = candidate.frame;
|
||||
prediction.result.contact_time = candidate.frame as f32 * STEP;
|
||||
prediction.result.contact_normal = landing_normal(
|
||||
candidate.direction,
|
||||
reference_velocity,
|
||||
support_normal,
|
||||
landing_velocity_scalar,
|
||||
landing_max_angle,
|
||||
);
|
||||
}
|
||||
|
||||
///82D6AF58: bias by cross-rail velocity, bound against the investigated
|
||||
///support normal, reject the rail axis, then normalize.
|
||||
pub fn landing_normal(
|
||||
direction: Vector,
|
||||
velocity: Vector,
|
||||
support: Vector,
|
||||
velocity_scalar: f32,
|
||||
maximum_angle_degrees: f32,
|
||||
) -> Vector {
|
||||
let across = sub(velocity, scale(direction, dot(direction, velocity)));
|
||||
let target = madd(across, -velocity_scalar, UP);
|
||||
let limited = clamp_angle(
|
||||
target,
|
||||
support,
|
||||
maximum_angle_degrees * f32::from_bits(0x3c8e_fa35),
|
||||
);
|
||||
normalize(sub(limited, scale(direction, dot(limited, direction))))
|
||||
}
|
||||
|
||||
///82BD3E78, shared with the offboard surface-frame port.
|
||||
fn clamp_angle(target: Vector, reference: Vector, limit: f32) -> Vector {
|
||||
let a = normalize(target);
|
||||
let b = normalize(reference);
|
||||
let axis = cross(a, b);
|
||||
let angle = crate::trigonometry::acos(dot(a, b).clamp(-1.0, 1.0));
|
||||
let turns = angle.mul_add(f32::from_bits(0x3e22_f983), 0.5).floor();
|
||||
let wrapped = (-turns).mul_add(f32::from_bits(0x40c9_0fdb), angle);
|
||||
if wrapped.abs() < limit || f32::from_bits(0x3780_0000) > dot(axis, axis) {
|
||||
return target;
|
||||
}
|
||||
let unit = normalize(axis);
|
||||
let (sin, cos) = crate::trigonometry::sin_cos(-limit * 0.5);
|
||||
let q = scale(unit, sin);
|
||||
scale(
|
||||
madd(
|
||||
cross(q, madd(reference, cos, cross(q, reference))),
|
||||
2.0,
|
||||
reference,
|
||||
),
|
||||
length(target),
|
||||
)
|
||||
}
|
||||
|
||||
///Admission and displacement portion82D6A840. The caller retries the next
|
||||
///ranked candidate on None, then applies82D609E0 and82D6AF58 on success.
|
||||
///This deliberately requires native surface evidence before any correction.
|
||||
pub fn admitted_displacement(
|
||||
prediction: Prediction, candidate: GrindTrajectoryCandidate, edge: Primitive,
|
||||
reference_velocity: Vector, processed_592: Vector, surface: GrindSurfaceEvidence,
|
||||
prediction: Prediction,
|
||||
candidate: GrindTrajectoryCandidate,
|
||||
edge: Primitive,
|
||||
reference_velocity: Vector,
|
||||
processed_592: Vector,
|
||||
surface: GrindSurfaceEvidence,
|
||||
limits: &GrindAssistLimits,
|
||||
) -> Option<Vector> {
|
||||
let natural_landing=prediction.request.trajectory.position_at(prediction.result.contact_time);
|
||||
if candidate.point[1]-natural_landing[1]<f32::from_bits(0xbecc_cccd)
|
||||
|| candidate.direction[1]>0.9 { return None; }
|
||||
let direction=normalize(sub(edge.end,edge.start));
|
||||
let perpendicular=sub(reference_velocity,scale(direction,dot(direction,reference_velocity)));
|
||||
if (perpendicular[1]*(1.0-direction[1].abs())).abs()>limits.max_downward_speed {
|
||||
let natural_landing = prediction
|
||||
.request
|
||||
.trajectory
|
||||
.position_at(prediction.result.contact_time);
|
||||
if candidate.point[1] - natural_landing[1] < f32::from_bits(0xbecc_cccd)
|
||||
|| candidate.direction[1] > 0.9
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if surface.kind==3 { return None; }
|
||||
let mut horizontal=perpendicular; horizontal[1]=0.0;
|
||||
let max_speed=if surface.kind==2 {limits.max_speed_squared_ledge} else {limits.max_speed_squared_rail};
|
||||
if dot(horizontal,horizontal)>max_speed { return None; }
|
||||
let delta=sub(candidate.trajectory_point,candidate.point);
|
||||
let lock_distance=if surface.kind==2 {
|
||||
let incoming=dot(delta,surface.side)>0.0;
|
||||
let body=dot(sub(processed_592,edge.start),surface.side)>0.0;
|
||||
limits.lock_distance*limits.ledge_scalars[if incoming {0} else {2}+if body {0} else {1}]
|
||||
} else {limits.lock_distance};
|
||||
if candidate.distance>=lock_distance.max(0.1) { return None; }
|
||||
let miss=sub(candidate.trajectory_point,edge.start);
|
||||
let mut correction=scale(sub(miss,scale(candidate.direction,dot(candidate.direction,miss))),-1.0);
|
||||
let distance=length(correction);
|
||||
if distance>=limits.lock_distance {
|
||||
let half_dimension=limits.deck_dimensions[1].mul_add(0.5,limits.deck_dimensions[0]*0.5);
|
||||
correction=scale(correction,(-limits.tip_scalar).mul_add(half_dimension,distance)/distance);
|
||||
let direction = normalize(sub(edge.end, edge.start));
|
||||
let perpendicular = sub(
|
||||
reference_velocity,
|
||||
scale(direction, dot(direction, reference_velocity)),
|
||||
);
|
||||
if (perpendicular[1] * (1.0 - direction[1].abs())).abs() > limits.max_downward_speed {
|
||||
return None;
|
||||
}
|
||||
if surface.kind == 3 {
|
||||
return None;
|
||||
}
|
||||
let mut horizontal = perpendicular;
|
||||
horizontal[1] = 0.0;
|
||||
let max_speed = if surface.kind == 2 {
|
||||
limits.max_speed_squared_ledge
|
||||
} else {
|
||||
limits.max_speed_squared_rail
|
||||
};
|
||||
if dot(horizontal, horizontal) > max_speed {
|
||||
return None;
|
||||
}
|
||||
let delta = sub(candidate.trajectory_point, candidate.point);
|
||||
let lock_distance = if surface.kind == 2 {
|
||||
let incoming = dot(delta, surface.side) > 0.0;
|
||||
let body = dot(sub(processed_592, edge.start), surface.side) > 0.0;
|
||||
limits.lock_distance
|
||||
* limits.ledge_scalars[if incoming { 0 } else { 2 } + if body { 0 } else { 1 }]
|
||||
} else {
|
||||
limits.lock_distance
|
||||
};
|
||||
if candidate.distance >= lock_distance.max(0.1) {
|
||||
return None;
|
||||
}
|
||||
let miss = sub(candidate.trajectory_point, edge.start);
|
||||
let mut correction = scale(
|
||||
sub(
|
||||
miss,
|
||||
scale(candidate.direction, dot(candidate.direction, miss)),
|
||||
),
|
||||
-1.0,
|
||||
);
|
||||
let distance = length(correction);
|
||||
if distance >= limits.lock_distance {
|
||||
let half_dimension =
|
||||
limits.deck_dimensions[1].mul_add(0.5, limits.deck_dimensions[0] * 0.5);
|
||||
correction = scale(
|
||||
correction,
|
||||
(-limits.tip_scalar).mul_add(half_dimension, distance) / distance,
|
||||
);
|
||||
}
|
||||
let original = prediction.request.trajectory.velocity;
|
||||
let adjusted = madd(correction, reciprocal(candidate.time), original);
|
||||
let angle = if dot(original, original) * dot(adjusted, adjusted) > f32::from_bits(0x38d1_b717) {
|
||||
angle_between(original, adjusted)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if angle > limits.maximum_adjust_angle * f32::from_bits(0x3c8e_fa35) {
|
||||
return None;
|
||||
}
|
||||
let original=prediction.request.trajectory.velocity;
|
||||
let adjusted=madd(correction,reciprocal(candidate.time),original);
|
||||
let angle=if dot(original,original)*dot(adjusted,adjusted)>f32::from_bits(0x38d1_b717) {
|
||||
angle_between(original,adjusted)
|
||||
} else {0.0};
|
||||
if angle>limits.maximum_adjust_angle*f32::from_bits(0x3c8e_fa35) { return None; }
|
||||
Some(correction)
|
||||
}
|
||||
|
||||
@@ -84,103 +197,132 @@ pub fn admitted_displacement(
|
||||
/// A double root must be strictly positive. The two-root branch takes max;
|
||||
/// it does not add a positive-time restriction absent from the source.
|
||||
pub fn descending_plane_time(t: Trajectory, point: Vector, normal: Vector) -> Option<f32> {
|
||||
let c = dot(normal,t.position)-dot(point,normal);
|
||||
let b = dot(normal,t.velocity);
|
||||
let a = dot(normal,scale(t.acceleration,reciprocal(2.0)));
|
||||
let discriminant = b*b - (4.0*a)*c;
|
||||
if discriminant < 0.0 { return None; }
|
||||
let inverse = reciprocal(2.0*a);
|
||||
let c = dot(normal, t.position) - dot(point, normal);
|
||||
let b = dot(normal, t.velocity);
|
||||
let a = dot(normal, scale(t.acceleration, reciprocal(2.0)));
|
||||
let discriminant = b * b - (4.0 * a) * c;
|
||||
if discriminant < 0.0 {
|
||||
return None;
|
||||
}
|
||||
let inverse = reciprocal(2.0 * a);
|
||||
if discriminant > 0.0 {
|
||||
let root = discriminant*inverse_length(discriminant);
|
||||
Some((inverse*(-b+root)).max(inverse*(-b-root)))
|
||||
let root = discriminant * inverse_length(discriminant);
|
||||
Some((inverse * (-b + root)).max(inverse * (-b - root)))
|
||||
} else {
|
||||
let time = inverse * -b;
|
||||
(time>0.0).then_some(time)
|
||||
(time > 0.0).then_some(time)
|
||||
}
|
||||
}
|
||||
|
||||
///82D6A398. The padding is physics_trajectory+516, not a host snap radius.
|
||||
pub fn consider_grind_primitive(
|
||||
prediction: Prediction, edge: Primitive, primitive: usize, padding: f32,
|
||||
prediction: Prediction,
|
||||
edge: Primitive,
|
||||
primitive: usize,
|
||||
padding: f32,
|
||||
) -> Option<GrindTrajectoryCandidate> {
|
||||
let mut delta = sub(edge.end,edge.start);
|
||||
let normal_unscaled = cross(delta,cross(UP,delta));
|
||||
let normal = scale(normal_unscaled,inverse_length(dot(normal_unscaled,normal_unscaled)));
|
||||
let mut delta = sub(edge.end, edge.start);
|
||||
let normal_unscaled = cross(delta, cross(UP, delta));
|
||||
let normal = scale(
|
||||
normal_unscaled,
|
||||
inverse_length(dot(normal_unscaled, normal_unscaled)),
|
||||
);
|
||||
let rail_length = length(delta);
|
||||
//830BD320 is initialized to1 by82F825D0. The original endpoints remain
|
||||
//unchanged; only the direction and endpoint overrun vector are capped.
|
||||
if rail_length>1.0 { delta=scale(delta,reciprocal(rail_length)); }
|
||||
let offset = madd(normal,prediction.request.radius,scale(normal,padding));
|
||||
let time = descending_plane_time(prediction.request.trajectory,add(edge.start,offset),normal)?;
|
||||
let trajectory_point = sub(prediction.request.trajectory.position_at(time),offset);
|
||||
if rail_length > 1.0 {
|
||||
delta = scale(delta, reciprocal(rail_length));
|
||||
}
|
||||
let offset = madd(normal, prediction.request.radius, scale(normal, padding));
|
||||
let time = descending_plane_time(
|
||||
prediction.request.trajectory,
|
||||
add(edge.start, offset),
|
||||
normal,
|
||||
)?;
|
||||
let trajectory_point = sub(prediction.request.trajectory.position_at(time), offset);
|
||||
let direction = normalize(delta);
|
||||
let difference = sub(trajectory_point,edge.start);
|
||||
let distance = length(cross(difference,direction));
|
||||
let point = madd(direction,dot(direction,difference),edge.start);
|
||||
let extension = scale(delta,0.1);
|
||||
let difference = sub(trajectory_point, edge.start);
|
||||
let distance = length(cross(difference, direction));
|
||||
let point = madd(direction, dot(direction, difference), edge.start);
|
||||
let extension = scale(delta, 0.1);
|
||||
//Strict endpoint test82D6A708. No closest-point clamping.
|
||||
if !(dot(sub(point,sub(edge.start,extension)),sub(point,add(edge.end,extension)))<0.0) {
|
||||
if !(dot(
|
||||
sub(point, sub(edge.start, extension)),
|
||||
sub(point, add(edge.end, extension)),
|
||||
) < 0.0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let velocity = prediction.request.trajectory.velocity;
|
||||
let approach = normalize(sub(velocity,scale(normal,dot(normal,velocity))));
|
||||
let mut horizontal = sub(point,prediction.request.trajectory.position);
|
||||
horizontal[1]=0.0;
|
||||
let angle = folded_approach_angle(horizontal,direction,normal);
|
||||
let approach = normalize(sub(velocity, scale(normal, dot(normal, velocity))));
|
||||
let mut horizontal = sub(point, prediction.request.trajectory.position);
|
||||
horizontal[1] = 0.0;
|
||||
let angle = folded_approach_angle(horizontal, direction, normal);
|
||||
Some(GrindTrajectoryCandidate {
|
||||
point,trajectory_point,direction,approach,distance,time,angle,
|
||||
frame:(time*60.0) as i32,primitive,
|
||||
point,
|
||||
trajectory_point,
|
||||
direction,
|
||||
approach,
|
||||
distance,
|
||||
time,
|
||||
angle,
|
||||
frame: (time * 60.0) as i32,
|
||||
primitive,
|
||||
})
|
||||
}
|
||||
|
||||
///8296EC98's oriented angle then82E09C80's actual fractional-turn fold.
|
||||
///Keep the wrap arithmetic: acos(abs(dot)) is not binary32 equivalent.
|
||||
fn folded_approach_angle(a: Vector,b: Vector,normal: Vector) -> f32 {
|
||||
let mut angle=angle_between(a,b);
|
||||
let aa=dot(a,a); let bb=dot(b,b);
|
||||
if aa>f32::from_bits(0x38d1_b717) && bb>f32::from_bits(0x38d1_b717) {
|
||||
let unit=|v,square| {
|
||||
let r=crate::physics::native_arithmetic::reciprocal_square_root_estimate(square);
|
||||
scale(v,(r*0.5).mul_add((-square).mul_add(r*r,1.0),r))
|
||||
fn folded_approach_angle(a: Vector, b: Vector, normal: Vector) -> f32 {
|
||||
let mut angle = angle_between(a, b);
|
||||
let aa = dot(a, a);
|
||||
let bb = dot(b, b);
|
||||
if aa > f32::from_bits(0x38d1_b717) && bb > f32::from_bits(0x38d1_b717) {
|
||||
let unit = |v, square| {
|
||||
let r = crate::physics::native_arithmetic::reciprocal_square_root_estimate(square);
|
||||
scale(v, (r * 0.5).mul_add((-square).mul_add(r * r, 1.0), r))
|
||||
};
|
||||
if dot(cross(unit(a,aa),unit(b,bb)),normal)<0.0 {
|
||||
angle=f32::from_bits(0x40c9_0fdb)-angle;
|
||||
if dot(cross(unit(a, aa), unit(b, bb)), normal) < 0.0 {
|
||||
angle = f32::from_bits(0x40c9_0fdb) - angle;
|
||||
}
|
||||
}
|
||||
let turns=angle*f32::from_bits(0x3e22_f983);
|
||||
let fraction=turns-turns.floor();
|
||||
let wrapped=(fraction-if fraction>0.5 {1.0} else {0.0})*f32::from_bits(0x40c9_0fdb);
|
||||
let sign=if wrapped>0.0 {1.0} else {-1.0};
|
||||
let magnitude=wrapped*sign;
|
||||
let folded=if magnitude>f32::from_bits(0x3fc9_0fdb) {
|
||||
magnitude-std::f32::consts::PI
|
||||
} else {magnitude};
|
||||
(sign*folded).abs()
|
||||
let turns = angle * f32::from_bits(0x3e22_f983);
|
||||
let fraction = turns - turns.floor();
|
||||
let wrapped = (fraction - if fraction > 0.5 { 1.0 } else { 0.0 }) * f32::from_bits(0x40c9_0fdb);
|
||||
let sign = if wrapped > 0.0 { 1.0 } else { -1.0 };
|
||||
let magnitude = wrapped * sign;
|
||||
let folded = if magnitude > f32::from_bits(0x3fc9_0fdb) {
|
||||
magnitude - std::f32::consts::PI
|
||||
} else {
|
||||
magnitude
|
||||
};
|
||||
(sign * folded).abs()
|
||||
}
|
||||
|
||||
///82D6A168 consumes one winner per call. Retrying after a rejected target
|
||||
///must retain the remaining insertion order, not sort the list by distance.
|
||||
pub fn take_best_grind(
|
||||
candidates: &mut Vec<GrindTrajectoryCandidate>,
|
||||
difficulty_distance: f32, height_penalty: &PointGraph<8>,
|
||||
difficulty_distance: f32,
|
||||
height_penalty: &PointGraph<8>,
|
||||
) -> Option<GrindTrajectoryCandidate> {
|
||||
let mut chosen=None;
|
||||
let mut fallback=1000.0;
|
||||
let mut best_angle=std::f32::consts::PI;
|
||||
let mut height=-10000.0;
|
||||
for (i,candidate) in candidates.iter().enumerate() {
|
||||
if candidate.distance<difficulty_distance {
|
||||
let benefit=best_angle-candidate.angle;
|
||||
if benefit>height_penalty.evaluate(candidate.point[1]-height) {
|
||||
height=candidate.point[1];
|
||||
fallback=-1.0;
|
||||
best_angle=candidate.angle;
|
||||
chosen=Some(i);
|
||||
let mut chosen = None;
|
||||
let mut fallback = 1000.0;
|
||||
let mut best_angle = std::f32::consts::PI;
|
||||
let mut height = -10000.0;
|
||||
for (i, candidate) in candidates.iter().enumerate() {
|
||||
if candidate.distance < difficulty_distance {
|
||||
let benefit = best_angle - candidate.angle;
|
||||
if benefit > height_penalty.evaluate(candidate.point[1] - height) {
|
||||
height = candidate.point[1];
|
||||
fallback = -1.0;
|
||||
best_angle = candidate.angle;
|
||||
chosen = Some(i);
|
||||
}
|
||||
} else if candidate.distance<fallback {
|
||||
fallback=candidate.distance;
|
||||
chosen=Some(i);
|
||||
} else if candidate.distance < fallback {
|
||||
fallback = candidate.distance;
|
||||
chosen = Some(i);
|
||||
}
|
||||
}
|
||||
chosen.map(|i|candidates.remove(i))
|
||||
chosen.map(|i| candidates.remove(i))
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Geometry fields consumed by trajectory assist:82C20728,82C20C08,
|
||||
//!82C21828 and82C21930. Material/foot-placement flags have other consumers.
|
||||
use super::{grind::GrindSurfaceEvidence, math::*};
|
||||
use crate::math::Vector3;
|
||||
use crate::physics::{
|
||||
board_world::BoardWorld,
|
||||
ground_hang_geometry::{HangGeometryInput, hang_lines},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GrindSurface {
|
||||
pub evidence: GrindSurfaceEvidence,
|
||||
pub normal: Vector,
|
||||
}
|
||||
|
||||
///The query consists of six consecutive48-byte descriptors; descriptor4 is
|
||||
///the vertical centre line and descriptor5 the raised cross line. The optional
|
||||
///seventh descriptor is a separate caller probe. Result indexing stays intact.
|
||||
pub fn investigate(
|
||||
world: &BoardWorld,
|
||||
start: Vector,
|
||||
end: Vector,
|
||||
reference: Vector,
|
||||
deck_center_to_truck: f32,
|
||||
) -> Result<Option<GrindSurface>, &'static str> {
|
||||
let Some(lines) = hang_lines(
|
||||
HangGeometryInput {
|
||||
edge_start: xyz(start),
|
||||
edge_end: xyz(end),
|
||||
reference_point: xyz(reference),
|
||||
},
|
||||
deck_center_to_truck,
|
||||
) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut hits = [None; 6];
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
hits[i] = world.query_swept_line(line.start, line.end, line.radius)?;
|
||||
}
|
||||
let direction = normalize(sub(end, start));
|
||||
let up = normalize(cross(direction, cross(UP, direction)));
|
||||
let side = normalize(cross(UP, direction));
|
||||
//82C20ED8 reads result5 (stack448), not the centre probe atstack384.
|
||||
let kind = if (hits[0].is_some() && hits[1].is_some()) || hits[5].is_some() {
|
||||
3
|
||||
} else if hits[0].is_some() {
|
||||
if hits[2].is_some_and(|h| h.geometry.fraction < 0.65) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else if hits[1].is_some() {
|
||||
if hits[3].is_some_and(|h| h.geometry.fraction < 0.65) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let side = if hits[0].is_none() && hits[1].is_some() {
|
||||
scale(side, -1.0)
|
||||
} else {
|
||||
side
|
||||
};
|
||||
let centre = madd(direction, dot(sub(reference, start), direction), start);
|
||||
let far: [Vector; 2] = std::array::from_fn(|i| {
|
||||
let p = hits[i + 2].map_or(lines[i + 2].end, |h| h.geometry.position);
|
||||
[p.x, p.y, p.z, 0.0]
|
||||
});
|
||||
//82C212CC..82C213F4: clearance angle16degrees expands both sides.
|
||||
let clearance = f32::from_bits(0x3e8e_fa35);
|
||||
let angle0 = crate::trigonometry::acos(
|
||||
dot(scale(up, -1.0), normalize(sub(far[0], centre))).clamp(-1.0, 1.0),
|
||||
) + std::f32::consts::FRAC_PI_2
|
||||
+ clearance;
|
||||
let angle1 = std::f32::consts::TAU
|
||||
- crate::trigonometry::acos(
|
||||
dot(scale(up, -1.0), normalize(sub(far[1], centre))).clamp(-1.0, 1.0),
|
||||
)
|
||||
- std::f32::consts::FRAC_PI_2
|
||||
- clearance;
|
||||
let pi = std::f32::consts::PI;
|
||||
let normal = if angle0 < angle1 && angle0 < pi && angle1 > pi {
|
||||
up
|
||||
} else {
|
||||
let angle = if angle0 < angle1 {
|
||||
if (pi - angle0).abs() < (pi - angle1).abs() {
|
||||
angle0
|
||||
} else {
|
||||
angle1
|
||||
}
|
||||
} else {
|
||||
(angle0 + angle1) * 0.5
|
||||
};
|
||||
rotate(direction, scale(up, -1.0), angle)
|
||||
};
|
||||
Ok(Some(GrindSurface {
|
||||
evidence: GrindSurfaceEvidence { kind, side },
|
||||
normal,
|
||||
}))
|
||||
}
|
||||
|
||||
///82C1E220 constructs a half-angle quaternion and rotates the supplied vector.
|
||||
fn rotate(axis: Vector, value: Vector, angle: f32) -> Vector {
|
||||
let (sin, cos) = crate::trigonometry::sin_cos(angle * 0.5);
|
||||
let q = scale(axis, sin);
|
||||
madd(cross(q, madd(value, cos, cross(q, value))), 2.0, value)
|
||||
}
|
||||
fn xyz(v: Vector) -> Vector3 {
|
||||
Vector3::new(v[0], v[1], v[2])
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Host world storage and batch ownership are independent of console layout.
|
||||
mod launch;
|
||||
pub mod grind;
|
||||
pub mod grind_surface;
|
||||
mod math;
|
||||
mod prediction;
|
||||
mod query;
|
||||
|
||||
@@ -26,6 +26,101 @@ pub struct FiftyFiftyCandidate {
|
||||
pub primitive: usize,
|
||||
}
|
||||
|
||||
///82C1FB98: the longitudinal deck rectangle. Unlike the two truck rectangles,
|
||||
///this catches a rail crossing between the trucks (boardslides/lipslides).
|
||||
pub fn deck_contact(
|
||||
board: [V; 4],
|
||||
flags_2484: u32,
|
||||
above: f32,
|
||||
below: f32,
|
||||
deck_center_to_truck: f32,
|
||||
primitives: &[Primitive],
|
||||
) -> Option<TruckContact> {
|
||||
if flags_2484 & 0x0020_0000 != 0 {
|
||||
return None;
|
||||
}
|
||||
let centre = add(board[3], scale(board[1], above));
|
||||
let along = scale(board[2], deck_center_to_truck);
|
||||
let down = scale(board[1], -(above + below));
|
||||
let a = add(centre, along);
|
||||
let b = sub(centre, along);
|
||||
let c = add(a, down);
|
||||
let d = add(b, down);
|
||||
let mut best = None;
|
||||
let mut distance = 1_000_000.0;
|
||||
for (primitive, edge) in primitives.iter().enumerate() {
|
||||
let first = segment_triangle(edge.start, edge.end, [a, b, c]);
|
||||
let second = segment_triangle(edge.start, edge.end, [d, b, c]);
|
||||
if let Some(position) = first.or(second) {
|
||||
let delta = sub(position, board[3]);
|
||||
let square = dot3(delta, delta);
|
||||
if square < distance {
|
||||
distance = square;
|
||||
best = Some(TruckContact {
|
||||
position,
|
||||
primitive,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
///82D889A8 geometry admission. The state manager owns approach-angle and
|
||||
///candidate arbitration separately. No nearest-rail substitute is used.
|
||||
pub fn boardslide_candidate(
|
||||
board: [V; 4],
|
||||
contact: TruckContact,
|
||||
edge: Primitive,
|
||||
velocity: V,
|
||||
category: u32,
|
||||
manager_frames: i32,
|
||||
flags_2476: u32,
|
||||
deck_center_to_truck: f32,
|
||||
truck_to_wheel: f32,
|
||||
) -> Option<FiftyFiftyCandidate> {
|
||||
if flags_2476 & 0x4000_0000 != 0 {
|
||||
return None;
|
||||
}
|
||||
let delta = sub(edge.end, edge.start);
|
||||
let direction = scale(delta, dot3(delta, delta).sqrt().recip());
|
||||
if dot3(upright_normal(direction), board[1]) <= 0.65 {
|
||||
return None;
|
||||
}
|
||||
let depth = dot3(sub(board[3], contact.position), board[1]);
|
||||
if depth * depth >= f32::from_bits(0x3b6b_edfa) {
|
||||
return None;
|
||||
}
|
||||
if category == 100 && dot3(velocity, direction).abs() <= 0.75 && manager_frames <= 10 {
|
||||
return None;
|
||||
}
|
||||
let horizontal = sub(delta, scale(board[1], dot3(delta, board[1])));
|
||||
let length = dot3(horizontal, horizontal).sqrt();
|
||||
if !(length > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let horizontal = scale(horizontal, length.recip());
|
||||
let alignment = dot3(horizontal, board[2]).abs();
|
||||
let from = sub(contact.position, board[3]);
|
||||
let across = sub(from, scale(board[1], dot3(from, board[1])));
|
||||
let remaining = deck_center_to_truck - dot3(across, across).sqrt();
|
||||
let clearance = if alignment > 0.0 {
|
||||
remaining / alignment * (1.0 - alignment * alignment).sqrt()
|
||||
} else {
|
||||
1000000.0
|
||||
};
|
||||
if !(clearance > truck_to_wheel) {
|
||||
return None;
|
||||
}
|
||||
Some(FiftyFiftyCandidate {
|
||||
direction,
|
||||
centre: contact.position,
|
||||
front: edge.end,
|
||||
rear: edge.start,
|
||||
primitive: contact.primitive,
|
||||
})
|
||||
}
|
||||
|
||||
///82D89150's same-primitive contact branch, before shared admission82D886B8.
|
||||
/// Linked/adjacent primitive arbitration belongs to the caller; this boundary
|
||||
/// explicitly returns none for that case instead of guessing an owner.
|
||||
@@ -40,10 +135,14 @@ pub fn fifty_fifty_candidate(
|
||||
///82D89150 also accepts two segment indices when their hits pass its signed
|
||||
/// cross-rail separation check. Splines with multiple segments need this path.
|
||||
pub fn fifty_fifty_candidate_on_splines(
|
||||
board: [V; 4], balance: [f32; 2],
|
||||
contacts: [Option<TruckContact>; 2], primitives: &[Primitive],
|
||||
board: [V; 4],
|
||||
balance: [f32; 2],
|
||||
contacts: [Option<TruckContact>; 2],
|
||||
primitives: &[Primitive],
|
||||
) -> Option<FiftyFiftyCandidate> {
|
||||
let [Some(front), Some(rear)] = contacts else { return None };
|
||||
let [Some(front), Some(rear)] = contacts else {
|
||||
return None;
|
||||
};
|
||||
if balance[0].abs().max(balance[1].abs()) >= 0.9 {
|
||||
return None;
|
||||
}
|
||||
@@ -53,7 +152,9 @@ pub fn fifty_fifty_candidate_on_splines(
|
||||
let direction = scale(delta, dot3(delta, delta).sqrt().recip());
|
||||
let across = cross(upright_normal(direction), direction);
|
||||
//820641A8. This comparison is signed in the original routine.
|
||||
if dot3(sub(front.position, rear.position), across) > 0.1 { return None; }
|
||||
if dot3(sub(front.position, rear.position), across) > 0.1 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let front_depth = dot3(sub(board[3], front.position), board[1]);
|
||||
let rear_depth = dot3(sub(board[3], rear.position), board[1]);
|
||||
@@ -63,7 +164,9 @@ pub fn fifty_fifty_candidate_on_splines(
|
||||
let difference = sub(front.position, rear.position);
|
||||
let length = dot3(difference, difference).sqrt();
|
||||
// A degenerate pair cannot define a finite native grind frame.
|
||||
if !length.is_finite() || length == 0.0 { return None; }
|
||||
if !length.is_finite() || length == 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(FiftyFiftyCandidate {
|
||||
direction: scale(difference, length.recip()),
|
||||
centre: scale(add(front.position, rear.position), 0.5),
|
||||
@@ -78,8 +181,12 @@ pub fn upright_normal(direction: V) -> V {
|
||||
let cross_up = cross([0.0, 1.0, 0.0, 0.0], direction);
|
||||
let mut normal = cross(cross_up, direction);
|
||||
let length = dot3(normal, normal).sqrt();
|
||||
if length <= 0.0 { return [1.0, 0.0, 0.0, 0.0]; }
|
||||
if normal[1] < 0.0 { normal = scale(normal, -1.0); }
|
||||
if length <= 0.0 {
|
||||
return [1.0, 0.0, 0.0, 0.0];
|
||||
}
|
||||
if normal[1] < 0.0 {
|
||||
normal = scale(normal, -1.0);
|
||||
}
|
||||
scale(normal, length.recip())
|
||||
}
|
||||
|
||||
@@ -89,7 +196,9 @@ pub fn within_approach_angle(direction: V, velocity: V, degrees: f32) -> bool {
|
||||
let normal = upright_normal(direction);
|
||||
let projected = sub(velocity, scale(normal, dot3(velocity, normal)));
|
||||
let length = dot3(projected, projected).sqrt();
|
||||
if length <= 0.001 { return true; }
|
||||
if length <= 0.001 {
|
||||
return true;
|
||||
}
|
||||
let alignment = dot3(scale(projected, length.recip()), direction).abs();
|
||||
alignment > (degrees * f32::from_bits(0x3c8e_fa35)).cos()
|
||||
}
|
||||
@@ -133,7 +242,10 @@ pub fn truck_contacts(
|
||||
let squared = dot3(delta, delta);
|
||||
if distance[truck] > squared {
|
||||
distance[truck] = squared;
|
||||
result[truck] = Some(TruckContact { position, primitive });
|
||||
result[truck] = Some(TruckContact {
|
||||
position,
|
||||
primitive,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,23 +263,38 @@ pub fn segment_triangle(start: V, end: V, triangle: [V; 3]) -> Option<V> {
|
||||
let direction = sub(end, start);
|
||||
let normal = cross(ab, ac);
|
||||
let denominator = -dot3(direction, normal);
|
||||
if denominator == 0.0 { return None; }
|
||||
if denominator == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let from = sub(start, a);
|
||||
let inverse = 1.0 / denominator;
|
||||
let t = inverse * dot3(from, normal);
|
||||
if t < 0.0 || t > 1.0 { return None; }
|
||||
if t < 0.0 || t > 1.0 {
|
||||
return None;
|
||||
}
|
||||
let side = cross(from, direction);
|
||||
let u = inverse * dot3(ac, side);
|
||||
let v = (1.0 / -denominator) * dot3(ab, side);
|
||||
if u + v > 1.0 || u < 0.0 || v < 0.0 { return None; }
|
||||
if u + v > 1.0 || u < 0.0 || v < 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(core::array::from_fn(|i| direction[i].mul_add(t, start[i])))
|
||||
}
|
||||
|
||||
fn sub(a: V, b: V) -> V { core::array::from_fn(|i| a[i] - b[i]) }
|
||||
fn add(a: V, b: V) -> V { core::array::from_fn(|i| a[i] + b[i]) }
|
||||
fn scale(a: V, s: f32) -> V { a.map(|v| v * s) }
|
||||
fn sub(a: V, b: V) -> V {
|
||||
core::array::from_fn(|i| a[i] - b[i])
|
||||
}
|
||||
fn add(a: V, b: V) -> V {
|
||||
core::array::from_fn(|i| a[i] + b[i])
|
||||
}
|
||||
fn scale(a: V, s: f32) -> V {
|
||||
a.map(|v| v * s)
|
||||
}
|
||||
fn cross(a: V, b: V) -> V {
|
||||
[(-a[2]).mul_add(b[1], a[1] * b[2]),
|
||||
(-a[0]).mul_add(b[2], a[2] * b[0]),
|
||||
(-a[1]).mul_add(b[0], a[0] * b[1]), 0.0]
|
||||
[
|
||||
(-a[2]).mul_add(b[1], a[1] * b[2]),
|
||||
(-a[0]).mul_add(b[2], a[2] * b[0]),
|
||||
(-a[1]).mul_add(b[0], a[0] * b[1]),
|
||||
0.0,
|
||||
]
|
||||
}
|
||||
@@ -77,6 +77,49 @@ fn scale(a: V, scale: f32) -> V {
|
||||
a.map(|v| v * scale)
|
||||
}
|
||||
|
||||
///Boardslide82D419A0, static rail branch (investigation kind!=2).
|
||||
///Returns the native point forces in order. Translation comes from the stock
|
||||
///PhysGrindTranslation animation attribute, not controller magnitude.
|
||||
pub fn boardslide_control(
|
||||
position: V,
|
||||
point: V,
|
||||
direction: V,
|
||||
normal: V,
|
||||
velocity: V,
|
||||
translation: f32,
|
||||
total_mass: f32,
|
||||
exiting: bool,
|
||||
ledge: bool,
|
||||
) -> Vec<V> {
|
||||
let across = cross(direction, normal);
|
||||
let offset = dot3(sub(position, point), across);
|
||||
let inward = scale(across, if offset > 0.0 { -1.0 } else { 1.0 });
|
||||
if exiting {
|
||||
return if offset.abs() > 0.12 {
|
||||
vec![scale(inward, 201.0)]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
}
|
||||
let perpendicular = sub(velocity, scale(direction, dot3(velocity, direction)));
|
||||
if ledge {
|
||||
return vec![scale(across, translation * 25.0)];
|
||||
}
|
||||
if offset.abs() > 0.16 {
|
||||
let mut forces = vec![scale(inward, 10.0)];
|
||||
if dot3(inward, perpendicular) < 0.0 {
|
||||
let mut stop = scale(perpendicular, -(total_mass * 60.0));
|
||||
stop[1] = 0.0;
|
||||
forces.push(stop);
|
||||
}
|
||||
forces
|
||||
} else {
|
||||
let mut damping = scale(perpendicular, -20.0);
|
||||
damping[1] = 0.0;
|
||||
vec![scale(across, translation * 25.0), damping]
|
||||
}
|
||||
}
|
||||
|
||||
///82D73AB0 straight, static 50-50 branch: both support normals coincide,
|
||||
///primitive motion336 is zero. The manager age controls the native exit nudge.
|
||||
pub fn fifty_fifty_pop(
|
||||
|
||||
@@ -85,7 +85,8 @@ pub(super) fn advance(
|
||||
player_state::post_input_and_select(physics, skater)?;
|
||||
player_state::pre_state(physics, skater)?;
|
||||
match skater.player_state.current() {
|
||||
skate_core::player::state::PhysicalStateId::GrindFiftyFifty
|
||||
skate_core::player::state::PhysicalStateId::GrindBoardslide
|
||||
| skate_core::player::state::PhysicalStateId::GrindFiftyFifty
|
||||
| skate_core::player::state::PhysicalStateId::Nonspecific => super::grind::update(physics, skater)?,
|
||||
skate_core::player::state::PhysicalStateId::BipedAir => {
|
||||
super::offboard::air_state::update(physics, skater)?;
|
||||
|
||||
@@ -22,11 +22,18 @@ pub(crate) struct Runtime {
|
||||
pub candidate: Option<FiftyFiftyCandidate>,
|
||||
pub name: String,
|
||||
pub active: bool,
|
||||
pub kind: u32,
|
||||
test_depth_epsilon: f32,
|
||||
test_depth: f32,
|
||||
exiting: bool,
|
||||
surface_kind: u32,
|
||||
surface_normal: V,
|
||||
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],
|
||||
manager_age: f32,
|
||||
pub launched: bool,
|
||||
pub launch_velocity: V,
|
||||
@@ -35,7 +42,10 @@ pub(crate) struct Runtime {
|
||||
diagnostic_tick: u32,
|
||||
}
|
||||
impl Runtime {
|
||||
pub fn load(data: &Collections, map: Option<&skate_data::skate_map::SkateMap>) -> Result<Self, String> {
|
||||
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);
|
||||
@@ -44,6 +54,12 @@ impl Runtime {
|
||||
candidate: None,
|
||||
name: String::new(),
|
||||
active: false,
|
||||
kind: 0,
|
||||
test_depth_epsilon: data.float("physics_grinds", "default", "TestDepthEpsilon")?,
|
||||
test_depth: data.float("physics_grinds", "default", "TestDepth")?,
|
||||
exiting: false,
|
||||
surface_kind: 0,
|
||||
surface_normal: [0.0, 1.0, 0.0, 0.0],
|
||||
truck_to_wheel: data.float("physics_grinds", "default", "TruckToWheel")?,
|
||||
deck_to_truck: data.float("physics_grinds", "default", "DeckCenterToTruck")?,
|
||||
pin_vs_slope: PointGraph {
|
||||
@@ -61,6 +77,18 @@ impl Runtime {
|
||||
.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(),
|
||||
manager_age: 0.0,
|
||||
launched: false,
|
||||
launch_velocity: [0.; 4],
|
||||
@@ -79,7 +107,7 @@ impl Runtime {
|
||||
.map(|c| self.primitives[c.primitive].owner)
|
||||
.unwrap_or(u64::MAX);
|
||||
GrindState {
|
||||
kind: 0,
|
||||
kind: self.kind as i32,
|
||||
name: encode(self.name.as_bytes()),
|
||||
scoring_name: encode(self.name.as_bytes()),
|
||||
pathed_guid: owner,
|
||||
@@ -97,37 +125,106 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
runtime.cooldown = runtime.cooldown.saturating_sub(1);
|
||||
runtime.manager_age = (runtime.manager_age + 0.0035).min(1.0);
|
||||
let old = runtime.candidate;
|
||||
let old_kind = runtime.kind;
|
||||
runtime.candidate = None;
|
||||
runtime.diagnostic_tick = runtime.diagnostic_tick.wrapping_add(1);
|
||||
let gated = runtime.cooldown > 0 || skater.player_input.grind.disabled
|
||||
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 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,
|
||||
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]; }
|
||||
for hit in hits.iter_mut().flatten() {
|
||||
hit.primitive = nearby[hit.primitive];
|
||||
}
|
||||
let extra = &skater.animation_input.extra;
|
||||
let candidate = grind_contact::fifty_fifty_candidate_on_splines(
|
||||
board, [extra.grind_translation, extra.grind_stability_nudge], hits,
|
||||
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,
|
||||
)
|
||||
});
|
||||
//82D875A8 tries the current concrete state before the ordinary truck,
|
||||
//tip and centre-deck fallback sequence.
|
||||
let (candidate, kind) = if runtime.active && runtime.kind == 1 && slide.is_some() {
|
||||
(slide, 1)
|
||||
} else if fifty.is_some() {
|
||||
(fifty, 0)
|
||||
} else {
|
||||
(slide, 1)
|
||||
};
|
||||
// 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);
|
||||
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(candidate) = candidate else { return; };
|
||||
if gated {
|
||||
return;
|
||||
}
|
||||
let Some(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.
|
||||
@@ -142,20 +239,45 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
return;
|
||||
}
|
||||
runtime.candidate = Some(candidate);
|
||||
runtime.kind = kind;
|
||||
if kind == 1 {
|
||||
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)) => {
|
||||
runtime.surface_kind = surface.evidence.kind;
|
||||
runtime.surface_normal = surface.normal;
|
||||
}
|
||||
Ok(None) | Err(_) => {
|
||||
runtime.candidate = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
p.grind_position_1120 = candidate.centre.map(f32::to_bits);
|
||||
p.grind_direction_1136 = candidate.direction.map(f32::to_bits);
|
||||
if !runtime.active || old.is_none() {
|
||||
if !runtime.active || old.is_none() || old_kind != kind {
|
||||
let backwards = dot3(board[2], velocity) < 0.0;
|
||||
let side = dot3(sub(candidate.centre, board[3]), board[0]) >= 0.0;
|
||||
runtime.name = format!(
|
||||
"{}{}_50_50",
|
||||
if backwards { "BF_" } else { "" },
|
||||
if side { "FS" } else { "BS" }
|
||||
"{}{}_{}",
|
||||
if backwards && kind == 0 { "BF_" } else { "" },
|
||||
if side { "FS" } else { "BS" },
|
||||
if kind == 0 { "50_50" } else { "BOARD" }
|
||||
);
|
||||
}
|
||||
}
|
||||
pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
|
||||
physics.grind.active = skater.player_state.current() == PhysicalStateId::GrindFiftyFifty;
|
||||
physics.grind.active = matches!(
|
||||
skater.player_state.current(),
|
||||
PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindBoardslide
|
||||
);
|
||||
physics.grind.exiting = false;
|
||||
physics.grind.distance = 0.0;
|
||||
physics.grind.previous_position = super::solve::deck_frame(&physics.board)[3];
|
||||
skater.ground_lifecycle.skeleton_elapsed_16505 = true;
|
||||
@@ -199,7 +321,12 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
|
||||
.grind
|
||||
.candidate
|
||||
.ok_or("Grind pop has no primitive")?;
|
||||
let [min, max] = physics.grind.pop_heights[p.state_variant_index_2528 as usize];
|
||||
let heights = if physics.grind.kind == 1 {
|
||||
&physics.grind.boardslide_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),
|
||||
@@ -233,20 +360,47 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
|
||||
.grind
|
||||
.candidate
|
||||
.ok_or("Active grind lost its candidate before selection")?;
|
||||
let normal = grind_contact::upright_normal(c.direction);
|
||||
let normal = if physics.grind.kind == 1 {
|
||||
physics.grind.surface_normal
|
||||
} else {
|
||||
grind_contact::upright_normal(c.direction)
|
||||
};
|
||||
let across = cross(c.direction, normal);
|
||||
let force = grind_forces::lateral_pin(
|
||||
board,
|
||||
c.centre,
|
||||
across,
|
||||
velocity,
|
||||
800.,
|
||||
0.,
|
||||
0.07,
|
||||
true,
|
||||
physics.grind.pin_vs_slope.evaluate(dot3(normal, normal)),
|
||||
);
|
||||
append(physics, force)?;
|
||||
if physics.grind.kind == 1 {
|
||||
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,
|
||||
skater.animation_input.extra.grind_translation,
|
||||
mass,
|
||||
physics.grind.exiting,
|
||||
physics.grind.surface_kind == 2,
|
||||
) {
|
||||
append(physics, force)?;
|
||||
}
|
||||
} else {
|
||||
let force = grind_forces::lateral_pin(
|
||||
board,
|
||||
c.centre,
|
||||
across,
|
||||
velocity,
|
||||
800.,
|
||||
0.,
|
||||
0.07,
|
||||
true,
|
||||
physics.grind.pin_vs_slope.evaluate(dot3(normal, normal)),
|
||||
);
|
||||
append(physics, force)?;
|
||||
}
|
||||
//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(
|
||||
@@ -257,7 +411,11 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
|
||||
false,
|
||||
1.3,
|
||||
0,
|
||||
[50., 40., 40.],
|
||||
if physics.grind.kind == 1 {
|
||||
[55., 55., 60.]
|
||||
} else {
|
||||
[50., 40., 40.]
|
||||
},
|
||||
);
|
||||
append(physics, friction)?;
|
||||
let forward = if dot3(board[2], c.direction) > 0.0 {
|
||||
@@ -265,7 +423,20 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
|
||||
} else {
|
||||
c.direction.map(|x| -x)
|
||||
};
|
||||
let target = [cross(normal, forward), normal, forward, board[3]];
|
||||
let target = if physics.grind.kind == 1 {
|
||||
//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)
|
||||
};
|
||||
[right, normal, cross(right, normal), board[3]]
|
||||
} 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 {
|
||||
|
||||
@@ -12,7 +12,7 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
|
||||
skater.player_state.current(),
|
||||
PhysicalStateId::PhysicsGround
|
||||
| PhysicalStateId::PhysicsAir
|
||||
| PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::Nonspecific
|
||||
| PhysicalStateId::KnownAir
|
||||
| PhysicalStateId::GroundAnimation
|
||||
|
||||
@@ -13,7 +13,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
|
||||
state,
|
||||
PhysicalStateId::PhysicsGround
|
||||
| PhysicalStateId::PhysicsAir
|
||||
| PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::Nonspecific
|
||||
| PhysicalStateId::KnownAir
|
||||
| PhysicalStateId::GroundAnimation
|
||||
@@ -88,7 +88,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
|
||||
}
|
||||
let physical = &mut skater.player_input.physical;
|
||||
if physics.grind.active {
|
||||
physical.grinds.words_136_140 = [0, 1];
|
||||
physical.grinds.words_136_140 = [physics.grind.kind, 1];
|
||||
if physics.grind.launched {
|
||||
physical.air.launched_442=1;
|
||||
physical.air.launch_velocity_128=physics.grind.launch_velocity.map(f32::to_bits);
|
||||
|
||||
@@ -8,8 +8,8 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
|
||||
let p = &skater.player_input.processed;
|
||||
let input = StateSelectionInput {
|
||||
processed: ProcessedStateInput {
|
||||
//The live paired-truck scorer publishes native kind0 and candidate384.
|
||||
grind_type_1248: 0,
|
||||
//The retained contact owner publishes native kind and candidate384.
|
||||
grind_type_1248: physics.grind.kind,
|
||||
grind_candidate_1488: physics.grind.candidate.is_some(),
|
||||
grind_investigation_flags_1516: 0,
|
||||
field_1776: p.external_physics_1616.flags as i32,
|
||||
|
||||
@@ -15,7 +15,7 @@ impl PhysicalStateCalls for Calls {
|
||||
PhysicalStateId::Sleeping
|
||||
| PhysicalStateId::PhysicsGround
|
||||
| PhysicalStateId::PhysicsAir
|
||||
| PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::Nonspecific
|
||||
| PhysicalStateId::KnownAir
|
||||
| PhysicalStateId::GroundAnimation
|
||||
@@ -31,7 +31,7 @@ impl PhysicalStateCalls for Calls {
|
||||
call.state.state,
|
||||
PhysicalStateId::PhysicsGround
|
||||
| PhysicalStateId::PhysicsAir
|
||||
| PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::Nonspecific
|
||||
| PhysicalStateId::KnownAir
|
||||
| PhysicalStateId::GroundAnimation
|
||||
@@ -59,7 +59,7 @@ pub(super) fn set(
|
||||
PhysicalStateId::Sleeping => requested == PhysicalStateId::PhysicsGround,
|
||||
PhysicalStateId::PhysicsGround
|
||||
| PhysicalStateId::PhysicsAir
|
||||
| PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::Nonspecific
|
||||
| PhysicalStateId::KnownAir
|
||||
| PhysicalStateId::GroundAnimation
|
||||
@@ -71,7 +71,7 @@ pub(super) fn set(
|
||||
requested,
|
||||
PhysicalStateId::PhysicsGround
|
||||
| PhysicalStateId::PhysicsAir
|
||||
| PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
|
||||
| PhysicalStateId::Nonspecific
|
||||
| PhysicalStateId::KnownAir
|
||||
| PhysicalStateId::GroundAnimation
|
||||
@@ -157,7 +157,7 @@ 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::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::exit(physics, skater)?,
|
||||
PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | 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),
|
||||
PhysicalStateId::PhysicsGround => super::super::ground_exit::exit(physics, skater),
|
||||
@@ -172,7 +172,7 @@ pub(super) fn set(
|
||||
_ => unreachable!("state support checked before publication"),
|
||||
}
|
||||
match requested {
|
||||
PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::enter(physics, skater),
|
||||
PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::enter(physics, skater),
|
||||
PhysicalStateId::BipedAir => super::super::offboard::air_state::enter(physics, skater),
|
||||
PhysicalStateId::BipedGround => {
|
||||
super::super::offboard::ground_state::enter(physics, skater)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Boardslide contact integration
|
||||
|
||||
The live grind owner now supports paired-truck 50-50 and centre-deck boardslide
|
||||
contacts. The ordinary authored default-world splines and imported splines use
|
||||
the same primitive collection. This is an incremental playable port, not all
|
||||
retail grind families or established 1:1 parity.
|
||||
|
||||
Native TU3 source boundaries:
|
||||
|
||||
- 82C1FB98: longitudinal deck rectangle, TestDepthEpsilon592, TestDepth596,
|
||||
DeckCenterToTruck636, first-triangle precedence and closest valid hit.
|
||||
- 82D889A8: upright dot>.65, squared deck depth<.0036, low-speed ground gate,
|
||||
projected rail/deck clearance and truck-width exclusion.
|
||||
- 82D875A8: current-state preference followed by fallback contact paths.
|
||||
Only the implemented truck and centre-deck paths are currently connected.
|
||||
- 82D418B8/82D40890: right-axis alignment for boardslides, compared with
|
||||
forward-axis alignment for50-50. The native matrix interpolation weight is
|
||||
.1; the separate random orientation perturbation is not implemented here.
|
||||
- 82D419A0/82D3FD08: translation25, lateral damping20, off-centre force10,
|
||||
mass-scaled60Hz opposing-velocity correction and exit force201.
|
||||
- 82D41848: friction strengths55/55/60.
|
||||
- 82D41CE8: interpolated mode100/104 pop values, distinct from50-50 mode92/96.
|
||||
- 82D41D18: Grinds136 publishes kind1. Selector maps kind1 to state400.
|
||||
|
||||
Geometry investigation now retains six native probe indices. Native82C20C08
|
||||
reads the raised cross line at result5 when deciding the blocked geometry kind;
|
||||
the older ground-only helper incorrectly inspected result4 there. The new
|
||||
boardslide investigation does not reuse that incorrect classification shortcut.
|
||||
The existing ground-only caller has not been changed in this increment.
|
||||
|
||||
Still incomplete: other concrete grind state owners, full chromosome/name
|
||||
production (the existing approach-name convention remains), all material and
|
||||
engagement branches, and airborne GrindAirAdjust. The new trajectory admission,
|
||||
publication and landing-normal leaves remain disconnected from live airborne
|
||||
selection until that owner is complete. No unconditional snapping was added.
|
||||
|
||||
Validation: compilation only. The user has reserved all gameplay and stability
|
||||
playtesting for themselves; no automated gameplay or test suite was run.
|
||||
@@ -1,6 +1,7 @@
|
||||
# Native grind assist port status
|
||||
|
||||
The live game still supports the paired-truck50-50 path only. The new
|
||||
The live game now supports paired-truck50-50 and centre-deck boardslide contacts
|
||||
(see boardslide-port.md). The new
|
||||
`skate-core/src/air/trajectory/grind.rs` leaves are not connected to gameplay.
|
||||
They must not be presented as working magnetic assistance or all-grind support.
|
||||
|
||||
@@ -45,9 +46,10 @@ The core crate builds. The user has reserved gameplay testing for themselves.
|
||||
|
||||
## Proposed read-only reference trace
|
||||
|
||||
Awaiting user authorization because earlier instructions prohibit launching
|
||||
the recomp. No synthetic gameplay input is proposed; the user would approach
|
||||
and land on a rail in the reference game.
|
||||
A reference run was authorized and launched, but its controller backend did
|
||||
not expose the physical controller. The user requested continuation from code.
|
||||
No successful reference grind capture was obtained and no synthetic gameplay
|
||||
input was sent.
|
||||
|
||||
Capture query inputs and indexed hit validity/fraction/position/normal/surface
|
||||
at82C20728 and82C20C08, the classification at82C21930, ranked candidates and
|
||||
|
||||
Reference in new issue
Block a user