Initial work on ledge & improved baking of root motion. (#90)
Reviewed-on: #90 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #90.
This commit is contained in:
+176
-11
@@ -20,14 +20,16 @@ use crate::fighter::visual::FighterAnimations;
|
||||
use crate::fighter::{
|
||||
Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
|
||||
FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole,
|
||||
FighterInvulnerabilityState, FighterPendingRespawn, FighterPresentationSnap,
|
||||
FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity,
|
||||
FighterInvulnerabilityState, FighterLedgeGrabLockout, FighterPendingRespawn,
|
||||
FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation,
|
||||
FighterVelocity,
|
||||
};
|
||||
use crate::game_settings::GameSettings;
|
||||
use crate::input::FighterInput;
|
||||
use crate::match_loading::MatchConfig;
|
||||
use crate::match_runtime::MatchState;
|
||||
use crate::math::int::FGi32;
|
||||
use crate::math::rect::FGRect;
|
||||
use crate::math::vec::FGVec2;
|
||||
use crate::math::vec3::FGVec3;
|
||||
use crate::player::Player;
|
||||
@@ -52,6 +54,8 @@ pub mod run;
|
||||
pub mod turn;
|
||||
pub mod wait;
|
||||
pub mod walk;
|
||||
pub mod ledge_wait;
|
||||
pub mod ledge_action;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum SurfacePolicy {
|
||||
@@ -135,6 +139,7 @@ pub struct AnimationStatus {
|
||||
pub struct InterruptContext<'a> {
|
||||
pub input: &'a FighterInput,
|
||||
pub facing_direction: &'a mut FighterFacingDirection,
|
||||
pub ledge_grab_lockout: &'a mut FighterLedgeGrabLockout,
|
||||
pub game_settings: &'a GameSettings,
|
||||
pub fighter_attributes: &'a FighterAttributes,
|
||||
pub surface: &'a FighterSurfaceState,
|
||||
@@ -158,21 +163,38 @@ pub struct EnterContext<'a> {
|
||||
pub input: &'a mut FighterInput,
|
||||
pub hitboxes: &'a mut FighterHitboxes,
|
||||
pub velocity: &'a mut FighterVelocity,
|
||||
pub translation: &'a mut FighterTranslation,
|
||||
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
|
||||
pub stale_move_queue: &'a mut FighterStaleMoveQueue,
|
||||
pub facing_direction: &'a mut FighterFacingDirection,
|
||||
pub animation_frame: &'a mut FighterAnimationFrame,
|
||||
pub presentation_snap: &'a mut FighterPresentationSnap,
|
||||
pub surface: &'a mut FighterSurfaceState,
|
||||
pub vulnerability: &'a mut FighterInvulnerabilityState,
|
||||
pub fighter_attributes: &'a FighterAttributes,
|
||||
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
pub game_settings: &'a GameSettings,
|
||||
pub stage_collision: &'a StageCollision,
|
||||
baked_animations: &'a BakedFighterAnimations,
|
||||
model_scale: FGi32,
|
||||
}
|
||||
|
||||
impl EnterContext<'_> {
|
||||
pub fn play_animation(&mut self, kind: AnimKind, repeat: bool) {
|
||||
self.animation_frame.reset(kind, repeat);
|
||||
}
|
||||
|
||||
/// World-space root position authored for frame zero of the animation most
|
||||
/// recently selected with [`Self::play_animation`].
|
||||
pub fn initial_animation_root_position(&self) -> Option<FGVec2> {
|
||||
let position = self
|
||||
.baked_animations
|
||||
.initial_root_position(self.animation_frame.kind, self.model_scale)?;
|
||||
Some(
|
||||
self.translation.0
|
||||
+ animation_position_to_gameplay(position, self.facing_direction),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExitContext<'a> {
|
||||
@@ -195,18 +217,22 @@ pub struct UpdateContext<'a> {
|
||||
/// Root motion in the fighter's gameplay plane: X is world-right and Y is
|
||||
/// world-up. Animation +Z is forward and is mirrored by facing direction.
|
||||
pub animation_root_motion_delta: FGVec2,
|
||||
/// Current world-space root position implied by this animation frame.
|
||||
/// States opt into root motion by assigning this to [`FighterTranslation`].
|
||||
pub animation_root_position: Option<FGVec2>,
|
||||
pub animation_frame: u32,
|
||||
pub fighter_attributes: &'a FighterAttributes,
|
||||
pub attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
pub facing_direction: &'a FighterFacingDirection,
|
||||
pub stage_collision: &'a StageCollision,
|
||||
}
|
||||
|
||||
fn animation_root_motion_to_gameplay(
|
||||
delta: FGVec3,
|
||||
fn animation_position_to_gameplay(
|
||||
position: FGVec3,
|
||||
facing_direction: &FighterFacingDirection,
|
||||
) -> FGVec2 {
|
||||
FGVec2::new(delta.z * facing_direction.to_sign(), delta.y)
|
||||
FGVec2::new(position.z * facing_direction.to_sign(), position.y)
|
||||
}
|
||||
|
||||
impl UpdateContext<'_> {
|
||||
@@ -226,6 +252,8 @@ pub struct CollisionContext<'a> {
|
||||
pub stage_collision: &'a StageCollision,
|
||||
pub leave_ground_ecb_lock_duration: u32,
|
||||
pub presentation_snap: &'a mut FighterPresentationSnap,
|
||||
pub fighter_ledge_collision_box: FGRect,
|
||||
pub ledge_grab_lockout_active: bool,
|
||||
pub downward_stick_threshold: FGi32,
|
||||
}
|
||||
|
||||
@@ -247,6 +275,17 @@ impl CollisionContext<'_> {
|
||||
ignore_soft_platforms,
|
||||
)
|
||||
}
|
||||
pub fn air_collide_with_ledges(
|
||||
&self,
|
||||
) -> Option<crate::fighter::collision::AirCollisionWithLedgeResult> {
|
||||
if self.ledge_grab_lockout_active {
|
||||
return None;
|
||||
}
|
||||
crate::fighter::collision::air_collide_with_ledges(
|
||||
&self.fighter_ledge_collision_box,
|
||||
self.stage_collision,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fighter_states! {
|
||||
@@ -260,6 +299,8 @@ fighter_states! {
|
||||
JumpSquat => jump::JumpSquatState,
|
||||
Jump => jump::JumpState,
|
||||
Land => land::LandingState,
|
||||
LedgeWait => ledge_wait::LedgeWaitState,
|
||||
LedgeAction => ledge_action::LedgeActionState,
|
||||
AirDodge => air_dodge::AirDodgeState,
|
||||
Turn => turn::TurnState,
|
||||
GroundAttack => ground_attack::GroundAttackState,
|
||||
@@ -285,6 +326,7 @@ pub struct FighterFrameQuery {
|
||||
pub facing_direction: &'static mut FighterFacingDirection,
|
||||
pub animation_frame: &'static mut FighterAnimationFrame,
|
||||
pub vulnerability: &'static mut FighterInvulnerabilityState,
|
||||
pub ledge_grab_lockout: &'static mut FighterLedgeGrabLockout,
|
||||
pub fighter: &'static Fighter,
|
||||
pub bone_matrices: &'static FighterBoneMatrices,
|
||||
}
|
||||
@@ -320,6 +362,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
InterruptContext {
|
||||
input: &self.input,
|
||||
facing_direction: &mut self.facing_direction,
|
||||
ledge_grab_lockout: &mut self.ledge_grab_lockout,
|
||||
game_settings,
|
||||
fighter_attributes: attributes,
|
||||
animation,
|
||||
@@ -335,20 +378,28 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
attributes: &'a FighterAttributes,
|
||||
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
game_settings: &'a GameSettings,
|
||||
stage_collision: &'a StageCollision,
|
||||
baked_animations: &'a BakedFighterAnimations,
|
||||
model_scale: FGi32,
|
||||
) -> EnterContext<'a> {
|
||||
EnterContext {
|
||||
input: &mut self.input,
|
||||
hitboxes: &mut self.hitboxes,
|
||||
velocity: &mut self.velocity,
|
||||
knockback_velocity: &mut self.knockback_velocity,
|
||||
translation: &mut self.translation,
|
||||
stale_move_queue: &mut self.stale_move_queue,
|
||||
facing_direction: &mut self.facing_direction,
|
||||
animation_frame: &mut self.animation_frame,
|
||||
surface: &mut self.surface,
|
||||
vulnerability: &mut self.vulnerability,
|
||||
presentation_snap: &mut self.presentation_snap,
|
||||
fighter_attributes: attributes,
|
||||
attack_script_assets,
|
||||
game_settings,
|
||||
stage_collision,
|
||||
baked_animations,
|
||||
model_scale,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +418,16 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
attributes: &'a FighterAttributes,
|
||||
attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
stage_collision: &'a StageCollision,
|
||||
) -> UpdateContext<'a> {
|
||||
let animation_root_motion_delta = animation_position_to_gameplay(
|
||||
self.bone_matrices.root_motion_delta(),
|
||||
&self.facing_direction,
|
||||
);
|
||||
let animation_root_position = self
|
||||
.bone_matrices
|
||||
.root_motion_position()
|
||||
.map(|_| self.translation.0 + animation_root_motion_delta);
|
||||
UpdateContext {
|
||||
translation: &mut self.translation,
|
||||
prev_translation: &mut self.prev_translation,
|
||||
@@ -377,24 +437,28 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
surface: &mut self.surface,
|
||||
knockback_velocity: &mut self.knockback_velocity,
|
||||
game_settings,
|
||||
animation_root_motion_delta: animation_root_motion_to_gameplay(
|
||||
self.bone_matrices.root_motion_delta(),
|
||||
&self.facing_direction,
|
||||
),
|
||||
animation_root_motion_delta,
|
||||
animation_root_position,
|
||||
animation_frame: self.animation_frame.frame,
|
||||
vulnerability: &mut self.vulnerability,
|
||||
fighter_attributes: attributes,
|
||||
attack_scripts,
|
||||
attack_script_assets,
|
||||
facing_direction: &self.facing_direction,
|
||||
stage_collision: &stage_collision,
|
||||
}
|
||||
}
|
||||
|
||||
fn collision_context<'a>(
|
||||
&'a mut self,
|
||||
stage_collision: &'a StageCollision,
|
||||
fighter_manifest: &'a FighterManifest,
|
||||
game_settings: &'a GameSettings,
|
||||
) -> CollisionContext<'a> {
|
||||
let fighter_ledge_collision_box = FGRect {
|
||||
origin: self.translation.0 + fighter_manifest.ledge_grab_box_start,
|
||||
end: self.translation.0 + fighter_manifest.ledge_grab_box_end,
|
||||
};
|
||||
CollisionContext {
|
||||
translation: &mut self.translation,
|
||||
prev_translation: &self.prev_translation,
|
||||
@@ -409,6 +473,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
.fighter_common
|
||||
.leave_ground_ecb_frame_lock_duration,
|
||||
downward_stick_threshold: game_settings.input_common.run_stick_threshold,
|
||||
fighter_ledge_collision_box,
|
||||
ledge_grab_lockout_active: self.ledge_grab_lockout.is_active(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,6 +498,7 @@ pub struct StatefulFighterQuery {
|
||||
pub fn state_update_system(
|
||||
mut query: Query<StatefulFighterQuery>,
|
||||
game_settings: Res<GameSettings>,
|
||||
stage_collision: Res<StageCollision>,
|
||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||
manifests: Res<Assets<FighterManifest>>,
|
||||
) {
|
||||
@@ -455,6 +522,7 @@ pub fn state_update_system(
|
||||
&manifest.attributes,
|
||||
&attack_scripts,
|
||||
&fighter.attack_script_assets,
|
||||
&stage_collision,
|
||||
);
|
||||
state.update(&mut update_ctx);
|
||||
}
|
||||
@@ -466,7 +534,10 @@ fn transition_state(
|
||||
frame: &mut FighterFrameQueryItem,
|
||||
attributes: &FighterAttributes,
|
||||
attack_script_assets: &FighterAttackScriptAssets,
|
||||
stage_collision: &StageCollision,
|
||||
game_settings: &GameSettings,
|
||||
baked_animations: &BakedFighterAnimations,
|
||||
model_scale: FGi32,
|
||||
) {
|
||||
let old_name = current.name();
|
||||
let new_name = next.name();
|
||||
@@ -480,7 +551,14 @@ fn transition_state(
|
||||
.leave_ground_ecb_frame_lock_duration,
|
||||
);
|
||||
}
|
||||
next.on_enter(&mut frame.enter_context(attributes, attack_script_assets, game_settings));
|
||||
next.on_enter(&mut frame.enter_context(
|
||||
attributes,
|
||||
attack_script_assets,
|
||||
game_settings,
|
||||
stage_collision,
|
||||
baked_animations,
|
||||
model_scale,
|
||||
));
|
||||
*current = next;
|
||||
info!("{} -> {}", old_name, new_name);
|
||||
}
|
||||
@@ -508,6 +586,9 @@ pub fn state_interrupt_system(
|
||||
let manifest = fighter_manifests
|
||||
.get(&fighter.manifest)
|
||||
.expect("Fighter manifest should be valid");
|
||||
let baked = baked_animations
|
||||
.get(&animations.baked)
|
||||
.expect("Animation should exist");
|
||||
|
||||
let new_state = if let Some(reaction) = pending_hit_reaction.take() {
|
||||
hitstop.remaining_frames = *reaction.victim_hitstop_frames;
|
||||
@@ -579,7 +660,10 @@ pub fn state_interrupt_system(
|
||||
&mut frame,
|
||||
&manifest.attributes,
|
||||
attack_script_assets,
|
||||
&stage_collision,
|
||||
&game_settings,
|
||||
baked,
|
||||
manifest.model_scale,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -594,6 +678,7 @@ pub fn state_collision_interrupt_system(
|
||||
mut match_state: ResMut<MatchState>,
|
||||
stage_registry: Res<StageManifestRegistry>,
|
||||
stage_manifests: Res<Assets<StageManifest>>,
|
||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||
) {
|
||||
let Some(stage_manifest_handle) = stage_registry.get(match_config.stage) else {
|
||||
return;
|
||||
@@ -608,6 +693,7 @@ pub fn state_collision_interrupt_system(
|
||||
mut state,
|
||||
mut frame,
|
||||
fighter,
|
||||
animations,
|
||||
attack_script_assets,
|
||||
mut hitstop,
|
||||
mut pending_respawn,
|
||||
@@ -619,6 +705,9 @@ pub fn state_collision_interrupt_system(
|
||||
let manifest = fighter_manifests
|
||||
.get(&fighter.manifest)
|
||||
.expect("Fighter manifest should be valid");
|
||||
let baked = baked_animations
|
||||
.get(&animations.baked)
|
||||
.expect("Animation should exist");
|
||||
|
||||
if pending_respawn.0 {
|
||||
**pending_respawn = false;
|
||||
@@ -665,7 +754,10 @@ pub fn state_collision_interrupt_system(
|
||||
&mut frame,
|
||||
&manifest.attributes,
|
||||
attack_script_assets,
|
||||
&stage_collision,
|
||||
&game_settings,
|
||||
baked,
|
||||
manifest.model_scale,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -675,7 +767,7 @@ pub fn state_collision_interrupt_system(
|
||||
}
|
||||
|
||||
let new_state = state.check_collision_interrupt(
|
||||
&mut frame.collision_context(&stage_collision, &game_settings),
|
||||
&mut frame.collision_context(&stage_collision, manifest, &game_settings),
|
||||
);
|
||||
if let Some(new_state) = new_state {
|
||||
transition_state(
|
||||
@@ -684,8 +776,81 @@ pub fn state_collision_interrupt_system(
|
||||
&mut frame,
|
||||
&manifest.attributes,
|
||||
&attack_script_assets,
|
||||
&stage_collision,
|
||||
&game_settings,
|
||||
baked,
|
||||
manifest.model_scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fighter::ecb::FighterPreviousECB;
|
||||
use crate::stage::line::{StageLedge, StageLedgeSide, StageVertexID};
|
||||
|
||||
#[test]
|
||||
fn animation_positions_map_forward_to_facing_and_preserve_height() {
|
||||
let position = FGVec3::lit("99", "3", "5");
|
||||
|
||||
assert_eq!(
|
||||
animation_position_to_gameplay(position, &FighterFacingDirection::Right),
|
||||
FGVec2::lit("5", "3")
|
||||
);
|
||||
assert_eq!(
|
||||
animation_position_to_gameplay(position, &FighterFacingDirection::Left),
|
||||
FGVec2::lit("-5", "3")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ledge_lockout_suppresses_ledge_collision() {
|
||||
let ledge_id = StageVertexID {
|
||||
polygon: 0,
|
||||
vertex: 0,
|
||||
};
|
||||
let stage_collision = StageCollision {
|
||||
ledges: vec![StageLedge {
|
||||
id: ledge_id,
|
||||
position: FGVec2::ZERO,
|
||||
hang_side: StageLedgeSide::Left,
|
||||
}],
|
||||
..default()
|
||||
};
|
||||
let previous_translation = FighterPreviousTranslation::default();
|
||||
let input = FighterInput::default();
|
||||
let mut translation = FighterTranslation::default();
|
||||
let mut velocity = FighterVelocity::default();
|
||||
let mut surface = FighterSurfaceState::default();
|
||||
let mut ecb = FighterECB::default();
|
||||
let previous_ecb = FighterPreviousECB::from(ecb);
|
||||
let mut presentation_snap = FighterPresentationSnap::default();
|
||||
let mut context = CollisionContext {
|
||||
translation: &mut translation,
|
||||
prev_translation: &previous_translation,
|
||||
input: &input,
|
||||
velocity: &mut velocity,
|
||||
surface: &mut surface,
|
||||
ecb: &mut ecb,
|
||||
prev_ecb: &previous_ecb,
|
||||
stage_collision: &stage_collision,
|
||||
leave_ground_ecb_lock_duration: 0,
|
||||
presentation_snap: &mut presentation_snap,
|
||||
fighter_ledge_collision_box: FGRect {
|
||||
origin: FGVec2::lit("-1", "1"),
|
||||
end: FGVec2::lit("1", "-1"),
|
||||
},
|
||||
ledge_grab_lockout_active: true,
|
||||
downward_stick_threshold: FGi32::ONE,
|
||||
};
|
||||
|
||||
assert!(context.air_collide_with_ledges().is_none());
|
||||
context.ledge_grab_lockout_active = false;
|
||||
assert_eq!(
|
||||
context.air_collide_with_ledges().map(|result| result.ledge),
|
||||
Some(ledge_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user