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:
@@ -1,5 +1,6 @@
|
||||
use crate::fighter::state::jump::{self};
|
||||
use crate::fighter::state::land::{LandingState, LandingType};
|
||||
use crate::fighter::state::ledge_wait::LedgeWaitState;
|
||||
use crate::fighter::state::{
|
||||
CollisionContext, FighterState, InterruptContext, air_attack, air_dodge,
|
||||
};
|
||||
@@ -126,6 +127,15 @@ pub fn collision_interrupt(
|
||||
landing_type: LandingType,
|
||||
can_ignore_soft_platforms: bool,
|
||||
) -> Option<FighterState> {
|
||||
if ctx.velocity.y <= FGi32::ZERO {
|
||||
if let Some(ledge_result) = ctx.air_collide_with_ledges() {
|
||||
return Some(FighterState::LedgeWait(LedgeWaitState {
|
||||
airborne_state_common: AirborneStateCommon::default(),
|
||||
ledge_id: ledge_result.ledge,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let res = ctx.air_collide_with_stage(can_ignore_soft_platforms)?;
|
||||
let diff = res.hit_position - ctx.translation.0;
|
||||
ctx.ecb.bottom -= diff;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! ECS types so it stays cheap to reason about and to unit-test — it has to be
|
||||
//! bit-for-bit deterministic for rollback.
|
||||
|
||||
use crate::fighter::state::air_dodge::AirDodgeState;
|
||||
use crate::fighter::state::{
|
||||
CollisionContext, FighterState, InterruptContext, air, crouch, dash, fall, ground_attack, turn, wait, walk,
|
||||
};
|
||||
@@ -9,6 +10,7 @@ use crate::fighter::{
|
||||
FighterAttributes, FighterTranslation, FighterVelocity,
|
||||
};
|
||||
use crate::game_settings::GameSettings;
|
||||
use crate::input::FighterInput;
|
||||
use crate::math::int::{FGWide, FGi32};
|
||||
use crate::math::vec::FGVec2;
|
||||
use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType};
|
||||
@@ -245,7 +247,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::fighter::ecb::FighterPreviousECB;
|
||||
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterSurfaceState};
|
||||
use crate::stage::line::StagePolyType;
|
||||
use crate::math::rect::FGRect;
|
||||
use crate::stage::line::StagePolyType;
|
||||
use crate::input::FighterInput;
|
||||
|
||||
fn two_segment_floor() -> StagePoly {
|
||||
@@ -357,6 +360,7 @@ mod tests {
|
||||
fn grounded_collision_updates_surface_segment() {
|
||||
let stage_collision = crate::stage::line::StageCollision {
|
||||
stage_polys: vec![two_segment_floor()],
|
||||
..crate::stage::line::StageCollision::default()
|
||||
};
|
||||
let mut surface = FighterSurfaceState::Grounded {
|
||||
line_id: StageLineID {
|
||||
@@ -387,6 +391,11 @@ mod tests {
|
||||
leave_ground_ecb_lock_duration: 10,
|
||||
presentation_snap: &mut presentation_snap,
|
||||
downward_stick_threshold: FGi32::ONE,
|
||||
fighter_ledge_collision_box: FGRect {
|
||||
origin: FGVec2::ZERO,
|
||||
end: FGVec2::ZERO,
|
||||
},
|
||||
ledge_grab_lockout_active: false,
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
@@ -412,6 +421,7 @@ mod tests {
|
||||
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor),
|
||||
],
|
||||
)],
|
||||
..crate::stage::line::StageCollision::default()
|
||||
};
|
||||
let mut surface = FighterSurfaceState::Grounded {
|
||||
line_id: StageLineID {
|
||||
@@ -442,6 +452,11 @@ mod tests {
|
||||
leave_ground_ecb_lock_duration: 10,
|
||||
presentation_snap: &mut presentation_snap,
|
||||
downward_stick_threshold: FGi32::ONE,
|
||||
fighter_ledge_collision_box: FGRect {
|
||||
origin: FGVec2::ZERO,
|
||||
end: FGVec2::ZERO,
|
||||
},
|
||||
ledge_grab_lockout_active: false,
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
@@ -477,3 +492,27 @@ pub fn grounded_movement_standstill_common_interrupts(
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perform_wavedash(input: &FighterInput, game_settings: &GameSettings) -> FighterState {
|
||||
// If aiming sideways and in the deadzone, do a perfect wavedash
|
||||
// TODO: Improve this so we don't have to go through the airborne state first
|
||||
let mut wavedash_dir =
|
||||
input
|
||||
.get_last_frame()
|
||||
.movement
|
||||
.normalize_or_zero();
|
||||
let movement = input.get_last_frame().movement;
|
||||
if movement.y.is_zero() && !movement.x.is_zero() {
|
||||
wavedash_dir.y =
|
||||
-game_settings.input_common.stick_deadzone - FGi32::lit("-0.01");
|
||||
wavedash_dir.x = movement.x.signum();
|
||||
}
|
||||
|
||||
if wavedash_dir == FGVec2::ZERO {
|
||||
wavedash_dir = FGVec2::lit("0.0", "-1.0");
|
||||
}
|
||||
|
||||
FighterState::AirDodge(AirDodgeState {
|
||||
direction: wavedash_dir.normalize_or_zero(),
|
||||
})
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
use crate::{
|
||||
fighter::{
|
||||
animation::AnimKind, state::{
|
||||
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_dodge::AirDodgeState, ground, ground_attack,
|
||||
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, ground::{self}, ground_attack,
|
||||
},
|
||||
}, input::{DigitalAction, FighterCommands}, math::{int::FGi32, vec::FGVec2},
|
||||
}, input::{DigitalAction, FighterCommands},
|
||||
};
|
||||
#[derive(Debug, Hash, Clone, Copy)]
|
||||
pub enum JumpType {
|
||||
@@ -50,27 +50,7 @@ impl FighterStateImpl for JumpSquatState {
|
||||
}
|
||||
// Special handling for wavedash from jump squat
|
||||
if state_context.input.has_command(FighterCommands::Shield) {
|
||||
// If aiming sideways and in the deadzone, do a perfect wavedash
|
||||
// TODO: Improve this so we don't have to go through the airborne state first
|
||||
let mut wavedash_dir = state_context
|
||||
.input
|
||||
.get_last_frame()
|
||||
.movement
|
||||
.normalize_or_zero();
|
||||
let movement = state_context.input.get_last_frame().movement;
|
||||
if movement.y.is_zero() && !movement.x.is_zero() {
|
||||
wavedash_dir.y =
|
||||
-state_context.game_settings.input_common.stick_deadzone - FGi32::lit("-0.01");
|
||||
wavedash_dir.x = movement.x.signum();
|
||||
}
|
||||
|
||||
if wavedash_dir == FGVec2::ZERO {
|
||||
wavedash_dir = FGVec2::lit("0.0", "-1.0");
|
||||
}
|
||||
|
||||
Some(FighterState::AirDodge(AirDodgeState {
|
||||
direction: wavedash_dir.normalize_or_zero(),
|
||||
}))
|
||||
Some(ground::perform_wavedash(state_context.input, state_context.game_settings))
|
||||
} else if self.duration_counter >= state_context.fighter_attributes.jumpsquat_duration {
|
||||
let tap_jump_held = matches!(self.initiator, JumpStateInitiator::Tap)
|
||||
&& state_context.input.get_last_frame().movement.y
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
|
||||
use crate::fighter::animation::AnimKind;
|
||||
use crate::fighter::state::air::AirborneStateCommon;
|
||||
use crate::fighter::state::jump::{JumpState, JumpStateInitiator, JumpType};
|
||||
use crate::fighter::state::{FighterState, ground};
|
||||
use crate::fighter::FighterFacingDirection;
|
||||
use crate::input::FighterCommands;
|
||||
use crate::math::int::FGi32;
|
||||
use crate::math::vec::FGVec2;
|
||||
use crate::stage::line::{StageLedgeSide, StageLineID, StageVertexID};
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum LedgeAction {
|
||||
Jump(JumpStateInitiator)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct LedgeActionState {
|
||||
pub ledge_id: StageVertexID,
|
||||
pub ledge_action: LedgeAction,
|
||||
pub is_in_grounded_phase: bool,
|
||||
}
|
||||
|
||||
impl LedgeActionState {
|
||||
pub fn create_jump(ledge_id: StageVertexID, initiator: JumpStateInitiator) -> Self {
|
||||
LedgeActionState {
|
||||
ledge_id,
|
||||
ledge_action: LedgeAction::Jump(initiator),
|
||||
is_in_grounded_phase: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FighterStateImpl for LedgeActionState {
|
||||
const NAME: &'static str = "LedgeAction";
|
||||
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||
match &self.ledge_action {
|
||||
LedgeAction::Jump(initiator) => {
|
||||
if state_context.is_current_animation_finished() {
|
||||
if state_context.input.has_command(FighterCommands::Shield) {
|
||||
Some(ground::perform_wavedash(state_context.input, state_context.game_settings))
|
||||
} else {
|
||||
Some(FighterState::Jump(JumpState {
|
||||
jump_type: JumpType::DoubleJump,
|
||||
airborne_state_common: AirborneStateCommon::default(),
|
||||
initiator: initiator.clone()
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||
match self.ledge_action {
|
||||
LedgeAction::Jump(_) => {
|
||||
state_context.play_animation(AnimKind::LedgeJump, false);
|
||||
state_context.input.enable_hold_all(10);
|
||||
state_context.input.clear_command(FighterCommands::Jump);
|
||||
state_context.input.clear_command(FighterCommands::TapJump);
|
||||
},
|
||||
}
|
||||
|
||||
state_context.velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||
return;
|
||||
};
|
||||
state_context.translation.0 = ledge.position;
|
||||
*state_context.facing_direction = match ledge.hang_side {
|
||||
StageLedgeSide::Left => FighterFacingDirection::Right,
|
||||
StageLedgeSide::Right => FighterFacingDirection::Left,
|
||||
};
|
||||
if let Some(root_position) = state_context.initial_animation_root_position() {
|
||||
state_context.translation.0 = root_position;
|
||||
}
|
||||
state_context.presentation_snap.0 = true;
|
||||
}
|
||||
|
||||
fn update(&mut self, state_context: &mut UpdateContext) {
|
||||
if !self.is_in_grounded_phase {
|
||||
let root_pos = state_context.animation_root_position.unwrap_or_default();
|
||||
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let ledge_pos = ledge.position;
|
||||
let ledge_modifier = match ledge.hang_side {
|
||||
StageLedgeSide::Left => FGi32::ONE,
|
||||
StageLedgeSide::Right => FGi32::NEG_ONE,
|
||||
};
|
||||
|
||||
let normalized_root_position = root_pos - ledge_pos;
|
||||
let normalized_root_position = FGVec2::new(normalized_root_position.x * ledge_modifier, normalized_root_position.y);
|
||||
|
||||
if normalized_root_position.x.is_positive() && normalized_root_position.y.is_positive() {
|
||||
self.is_in_grounded_phase = true;
|
||||
|
||||
let segment_id = StageLineID {
|
||||
polygon: self.ledge_id.polygon,
|
||||
segment: self.ledge_id.vertex
|
||||
};
|
||||
let Some(poly) = state_context.stage_collision.get_segment(segment_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state_context.translation.0 = poly.segment.point1();
|
||||
state_context.surface.land(segment_id);
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_in_grounded_phase {
|
||||
state_context.velocity.0.x = state_context.animation_root_motion_delta.x;
|
||||
ground::apply_grounded_motion(
|
||||
state_context.velocity,
|
||||
state_context.translation,
|
||||
);
|
||||
} else {
|
||||
if let Some(root_position) = state_context.animation_root_position {
|
||||
state_context.translation.0 = root_position;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn check_collision_interrupt(
|
||||
&mut self,
|
||||
_state_context: &mut CollisionContext,
|
||||
) -> Option<FighterState> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_visually_grounded<'a>(&'a self) -> bool {
|
||||
self.is_in_grounded_phase
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
|
||||
use crate::fighter::animation::AnimKind;
|
||||
use crate::fighter::state::air::AirborneStateCommon;
|
||||
use crate::fighter::state::fall::FallState;
|
||||
use crate::fighter::state::FighterState;
|
||||
use crate::fighter::FighterFacingDirection;
|
||||
use crate::fighter::state::jump::JumpStateInitiator;
|
||||
use crate::fighter::state::ledge_action::{LedgeActionState};
|
||||
use crate::input::FighterCommands;
|
||||
use crate::math::vec::FGVec2;
|
||||
use crate::stage::line::{StageLedgeSide, StageVertexID};
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct LedgeWaitState {
|
||||
pub airborne_state_common: AirborneStateCommon,
|
||||
pub ledge_id: StageVertexID,
|
||||
}
|
||||
|
||||
impl FighterStateImpl for LedgeWaitState {
|
||||
const NAME: &'static str = "LedgeWait";
|
||||
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||
if state_context.input.has_command(FighterCommands::Jump) {
|
||||
return Some(FighterState::LedgeAction(LedgeActionState::create_jump(self.ledge_id, JumpStateInitiator::Button)));
|
||||
} else if state_context.input.has_command(FighterCommands::TapJump) {
|
||||
return Some(FighterState::LedgeAction(LedgeActionState::create_jump(self.ledge_id, JumpStateInitiator::Tap)));
|
||||
}
|
||||
if is_ledge_drop_input(state_context.input.get_last_frame().movement.x,
|
||||
state_context.game_settings.input_common.run_stick_threshold,
|
||||
state_context.facing_direction) {
|
||||
state_context.ledge_grab_lockout.start(
|
||||
state_context
|
||||
.game_settings
|
||||
.fighter_common
|
||||
.ledge_drop_regrab_lockout_duration_frames,
|
||||
);
|
||||
return Some(FighterState::Fall(FallState::new(
|
||||
AirborneStateCommon { is_fast_falling: false },
|
||||
)));
|
||||
}
|
||||
if is_ledge_drop_fastfall_input(
|
||||
state_context.input.get_last_frame().movement.y,
|
||||
state_context.game_settings.input_common.run_stick_threshold,
|
||||
) {
|
||||
state_context.ledge_grab_lockout.start(
|
||||
state_context
|
||||
.game_settings
|
||||
.fighter_common
|
||||
.ledge_drop_regrab_lockout_duration_frames,
|
||||
);
|
||||
return Some(FighterState::Fall(FallState::new(
|
||||
AirborneStateCommon { is_fast_falling: true },
|
||||
)));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||
state_context.velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||
return;
|
||||
};
|
||||
state_context.translation.0 = ledge.position;
|
||||
*state_context.facing_direction = match ledge.hang_side {
|
||||
StageLedgeSide::Left => FighterFacingDirection::Right,
|
||||
StageLedgeSide::Right => FighterFacingDirection::Left,
|
||||
};
|
||||
state_context.play_animation(AnimKind::LedgeWait, true);
|
||||
if let Some(root_position) = state_context.initial_animation_root_position() {
|
||||
state_context.translation.0 = root_position;
|
||||
}
|
||||
state_context.presentation_snap.0 = true;
|
||||
}
|
||||
|
||||
fn update(&mut self, state_context: &mut UpdateContext) {
|
||||
if let Some(root_position) = state_context.animation_root_position {
|
||||
state_context.translation.0 = root_position;
|
||||
}
|
||||
}
|
||||
|
||||
fn check_collision_interrupt(
|
||||
&mut self,
|
||||
_state_context: &mut CollisionContext,
|
||||
) -> Option<FighterState> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_visually_grounded<'a>(&'a self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::int::FGi32;
|
||||
|
||||
#[test]
|
||||
fn ledge_drop_uses_the_run_stick_threshold() {
|
||||
let threshold = FGi32::lit("0.625");
|
||||
|
||||
assert!(is_ledge_drop_fastfall_input(FGi32::lit("-0.625"), threshold));
|
||||
assert!(is_ledge_drop_fastfall_input(FGi32::lit("-1.0"), threshold));
|
||||
assert!(!is_ledge_drop_fastfall_input(FGi32::lit("-0.624"), threshold));
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ledge_drop_fastfall_input(
|
||||
vertical_input: crate::math::int::FGi32,
|
||||
threshold: crate::math::int::FGi32,
|
||||
) -> bool {
|
||||
vertical_input <= -threshold
|
||||
}
|
||||
|
||||
fn is_ledge_drop_input(
|
||||
horizontal_input: crate::math::int::FGi32,
|
||||
threshold: crate::math::int::FGi32,
|
||||
facing_dir: &FighterFacingDirection,
|
||||
) -> bool {
|
||||
let dir = facing_dir.to_sign();
|
||||
if horizontal_input.abs() > threshold {
|
||||
if dir != horizontal_input.signum() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
+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