diff --git a/crates/skate-game/src/graph_host/motion_conditions.rs b/crates/skate-game/src/graph_host/motion_conditions.rs index 1495382d..d3ff28ad 100644 --- a/crates/skate-game/src/graph_host/motion_conditions.rs +++ b/crates/skate-game/src/graph_host/motion_conditions.rs @@ -9,6 +9,7 @@ use skate_data::state_graph::attributes::Attributes; #[derive(Clone, Debug, PartialEq)] pub enum MotionCondition { + ManualOutTimerIsActive, Gesture(crate::input::gesture_catalog::Group), Landing(super::motion_landing::Condition), Wipeout(super::motion_wipeout::Condition), @@ -77,6 +78,7 @@ impl MotionCondition { } let numeric = || super::condition_nodes::numeric(a); Ok(Some(match a.text("name").unwrap_or("") { + "ManualOutTimerIsActive" => Self::ManualOutTimerIsActive, "HasGestureIntent" => Self::Gesture(crate::input::gesture_catalog::Group::parse( a.text("group").unwrap_or(""), )?), @@ -156,6 +158,8 @@ impl MotionCondition { &host.state_parents, ) .map_err(str::to_owned)?, + //82BA78B0 -> specific getter: strictly positive retained timer. + Self::ManualOutTimerIsActive => host.riding.manual_out_timer > 0.0, Self::Gameplay(condition) => condition.evaluate(host)?, Self::Riding(condition) => condition.evaluate(host)?, Self::TimeToLand(n) => n.matches( diff --git a/crates/skate-game/src/graph_host/motion_riding.rs b/crates/skate-game/src/graph_host/motion_riding.rs index 4de190ce..54a70871 100644 --- a/crates/skate-game/src/graph_host/motion_riding.rs +++ b/crates/skate-game/src/graph_host/motion_riding.rs @@ -60,6 +60,7 @@ pub enum RidingOperation { TimeSinceTeleport, TimeSinceKickturn, ManualOutTimer, + SetManualOutTimer(f32), } impl RidingOperation { pub fn parse(a: &Attributes<'_>) -> Option { @@ -103,6 +104,9 @@ impl RidingOperation { "UpdateTimeSinceTeleport" => Self::TimeSinceTeleport, "UpdateTimeSinceKickturn" => Self::TimeSinceKickturn, "UpdateManualOutTimer" => Self::ManualOutTimer, + "SetManualOutTimer" => Self::SetManualOutTimer(f32::from_bits( + a.float_bits("length", 0x3e29fbe7))), //82BCA058 constructor default + _ => return None, }) } @@ -214,6 +218,10 @@ impl RidingState { self.time_since_kickturn += frame.dt; } } //82BAE848/8258F940 + RidingOperation::SetManualOutTimer(length) => { + //82BB9158 is the End callback (vtable8232090C+56). + if phase == 2 { self.manual_out_timer = length; } + } RidingOperation::ManualOutTimer => { if phase == 2 { self.manual_out_timer = 0.0; diff --git a/crates/skate-game/src/graph_host/tests/motion_tricks.rs b/crates/skate-game/src/graph_host/tests/motion_tricks.rs index 2453dee3..40792426 100644 --- a/crates/skate-game/src/graph_host/tests/motion_tricks.rs +++ b/crates/skate-game/src/graph_host/tests/motion_tricks.rs @@ -51,6 +51,15 @@ fn stock_360_crouch_takeoff_and_air_animation_pipeline() { let mut cursor = Some(state); while let Some(i) = cursor { let state = &graph.binding.states[i]; + // Selection evaluates sibling preconditions before reaching FromAntic. + if let Some(parent) = state.parent { + for &sibling in graph.binding.states[parent].children.iter().filter(|_| + matches!(graph.binding.states[parent].name.as_str(), "Takeoff" | "360Flip")) { + if let Some(expression) = graph.binding.states[sibling].expression { + check_expression(&graph.binding, &implementations.operations, expression); + } + } + } if let Some(expression) = state.expression { check_expression(&graph.binding, &implementations.operations, expression); } @@ -89,6 +98,26 @@ fn stock_360_crouch_takeoff_and_air_animation_pipeline() { last: None, state_times: Vec::new(), }; + // The authored manual-exit behavior arms this timer on End, not Begin. + // Exercise its lifecycle with the existing update owner and condition. + use crate::graph_host::{motion_riding::RidingOperation, motion_conditions::MotionCondition}; + let timer = graph.binding.operations.iter().position(|op| op.name == "SetManualOutTimer").unwrap(); + let MotionOperation::Riding(set_timer) = host.operations[timer].clone() else { panic!("Timer factory"); }; + let active = MotionCondition::ManualOutTimerIsActive; + for phase in [0, 1] { + host.riding.execute(set_timer.clone(), phase, &frame, &mut host.animation, None, None, false).unwrap(); + assert!(!active.evaluate(&host, &frame).unwrap()); + } + host.riding.execute(set_timer, 2, &frame, &mut host.animation, None, None, false).unwrap(); + assert_eq!(host.riding.manual_out_timer, 0.1); + assert!(active.evaluate(&host, &frame).unwrap()); + host.riding.execute(RidingOperation::ManualOutTimer, 1, &frame, &mut host.animation, None, None, true).unwrap(); + assert_eq!(host.riding.manual_out_timer, 0.1); + for _ in 0..7 { + host.riding.execute(RidingOperation::ManualOutTimer, 1, &frame, &mut host.animation, None, None, false).unwrap(); + } + assert_eq!(host.riding.manual_out_timer, 0.0); + assert!(!active.evaluate(&host, &frame).unwrap()); let evaluation = Evaluation { cull_threshold: 0.01, update_history: true, diff --git a/docs/360-FLIP.md b/docs/360-FLIP.md index ca740aa2..f974e4ed 100644 --- a/docs/360-FLIP.md +++ b/docs/360-FLIP.md @@ -88,3 +88,10 @@ connected catalog. Advanced manual/grind-out branches still contain the known unsupported `JumpInto` behavior, and the existing separate `AddRunoutAttribs` gap remains. Those handlers were not bypassed. The current test does not certify dark catches or the rest of the trick catalog visually. + +The subsequent user run reached `ManualOutTimerIsActive` at tick3854. +This condition now reads the existing retained timer (>0, native82BA78B0 +and8258F978). `SetManualOutTimer` arms it on End (82BB9158/8258F990), +using the authored length; UpdateManualOutTimer already owns decay/reset. +The regression fixture covers arming, hold during manuals, expiry, and +sibling takeoff preconditions evaluated before FromAntic selection.