Files
shinespark/src/fighter/motion.rs
T
EIREXE 2b5af49e15
CI / build-and-test (push) Successful in 1m22s
Implement hitstop shake (#42)
Reviewed-on: #42
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-18 11:56:53 +00:00

197 lines
6.9 KiB
Rust

//! Where a fighter is and how it moves. States write [`FighterVelocity`];
//! this module is what turns that into a position.
use bevy::prelude::*;
use bevy_ggrs::GgrsFrameTiming;
use crate::debug_tools::PresentationMotionMode;
use crate::fighter::{FighterHitstopShake};
use crate::game_settings::GameSettings;
use crate::{
fighter::{FighterECB, FighterFacingDirection, baked_animation::FixedMat4},
math::{int::FGi32, vec::FGVec2, vec3::FGVec3},
};
#[derive(Component, Clone, Copy, Hash)]
pub struct FighterPreviousFacingDirection(pub FighterFacingDirection);
#[derive(Component, Clone, Copy, Hash)]
pub struct Grounded;
#[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy, Hash)]
pub struct FighterVelocity(pub FGVec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)]
pub struct FighterPreviousTranslation(pub FGVec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)]
#[require(FighterPreviousTranslation)]
pub struct FighterTranslation(pub FGVec2);
impl FighterTranslation {
pub fn get_3d_transform(&self, facing_dir: &FighterFacingDirection) -> FixedMat4 {
let mut o = match facing_dir {
FighterFacingDirection::Left => FixedMat4::IDENTITY.rotate_y(3),
FighterFacingDirection::Right => FixedMat4::IDENTITY.rotate_y(1),
};
o.translate(FGVec3::new(self.0.x, self.0.y, FGi32::ZERO));
o
}
}
/// Samples deterministic fighter positions for presentation without changing
/// gameplay state or rollback timing.
pub fn sample_presentation_translation(
previous: FighterPreviousTranslation,
current: FighterTranslation,
overstep_fraction: f32,
mode: PresentationMotionMode,
) -> Vec2 {
let previous = Vec2::new(previous.x.to_num(), previous.y.to_num());
let current = Vec2::new(current.x.to_num(), current.y.to_num());
match mode {
PresentationMotionMode::Interpolation => previous.lerp(current, overstep_fraction),
PresentationMotionMode::Extrapolation => current + (current - previous) * overstep_fraction,
}
}
pub fn apply_fighter_translation_to_visuals(
query: Query<(
&FighterTranslation,
&FighterPreviousTranslation,
&FighterECB,
&FighterHitstopShake,
&mut Transform,
)>,
timing: Res<GgrsFrameTiming>,
debug_settings: Res<crate::debug_tools::DebugSettings>,
game_settings: Res<GameSettings>,
) {
let overstep_fraction = timing.overstep_fraction();
for (translation, previous_translation, ecb, hitstop_shake, mut transform) in query {
let translation = sample_presentation_translation(
*previous_translation,
*translation,
overstep_fraction,
debug_settings.motion_sampling,
);
let bottom = ecb.get_bottom_point();
transform.translation = Vec3::new(
translation.x + bottom.x.to_num::<f32>(),
translation.y + bottom.y.to_num::<f32>(),
0.0,
);
if hitstop_shake.remaining_frames > 0 && hitstop_shake.total_frames > 0 && !game_settings.fighter_common.visual_shake_table.is_empty() {
let total = hitstop_shake.total_frames as f32;
let curr = hitstop_shake.remaining_frames as f32;
let elapsed = 1.0 - curr / total;
let access_index = (hitstop_shake.remaining_frames as usize) % game_settings.fighter_common.visual_shake_table.len();
let shake_multiplier = match hitstop_shake.role {
super::FighterHitstopShakeRole::Attacker => game_settings.fighter_common.attacker_visual_shake_multiplier,
super::FighterHitstopShakeRole::Receiver => 1.0,
};
let shake_amount: f32 = 1.0 - EaseFunction::QuinticIn.sample(elapsed).unwrap_or_default();
let shake_amount = game_settings.fighter_common.visual_shake_table[access_index] * shake_amount * shake_multiplier;
let shake_offset = match hitstop_shake.kind {
super::FighterHitstopShakeKind::Horizontal => Vec3::new(shake_amount, 0.0, 0.0),
super::FighterHitstopShakeKind::Vertical => Vec3::new(0.0, shake_amount, 0.0),
};
transform.translation += shake_offset;
}
}
}
pub fn apply_air_motion(
fighters: Query<(
&FighterVelocity,
&mut FighterTranslation,
&mut FighterPreviousTranslation,
)>,
_time: Res<Time<Fixed>>,
) {
for (velocity, mut translation, mut prev_translation) in fighters {
prev_translation.0 = translation.0;
translation.0 += velocity.0;
}
}
pub fn fighter_movement() {}
pub fn snapshot_fighter_motion(
query: Query<(
&FighterTranslation,
&FighterFacingDirection,
&mut FighterPreviousTranslation,
&mut FighterPreviousFacingDirection,
)>,
) {
for (translation, facing, mut previous_translation, mut previous_facing) in query {
previous_translation.0 = translation.0;
previous_facing.0 = *facing;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presentation_translation_interpolates_between_fixed_samples() {
let position = sample_presentation_translation(
FighterPreviousTranslation(FGVec2::new(FGi32::lit("2"), FGi32::lit("4"))),
FighterTranslation(FGVec2::new(FGi32::lit("6"), FGi32::lit("12"))),
0.25,
PresentationMotionMode::Interpolation,
);
assert_eq!(position, Vec2::new(3.0, 6.0));
}
#[test]
fn presentation_translation_extrapolates_from_current_sample() {
let position = sample_presentation_translation(
FighterPreviousTranslation(FGVec2::new(FGi32::lit("2"), FGi32::lit("4"))),
FighterTranslation(FGVec2::new(FGi32::lit("6"), FGi32::lit("12"))),
0.25,
PresentationMotionMode::Extrapolation,
);
assert_eq!(position, Vec2::new(7.0, 14.0));
}
#[test]
fn snapshot_fighter_motion_copies_current_world_state() {
let mut world = World::new();
world.spawn((
FighterTranslation(FGVec2::new(FGi32::lit("3"), FGi32::lit("4"))),
FighterFacingDirection::Left,
FighterPreviousTranslation::default(),
FighterPreviousFacingDirection(FighterFacingDirection::Right),
));
let mut schedule = Schedule::default();
schedule.add_systems(snapshot_fighter_motion);
schedule.run(&mut world);
let (_, _, previous_translation, previous_facing) = world
.query::<(
&FighterTranslation,
&FighterFacingDirection,
&FighterPreviousTranslation,
&FighterPreviousFacingDirection,
)>()
.single(&world)
.unwrap();
assert_eq!(
previous_translation.0,
FGVec2::new(FGi32::lit("3"), FGi32::lit("4"))
);
assert_eq!(previous_facing.0, FighterFacingDirection::Left);
}
}