204 lines
7.0 KiB
Rust
204 lines
7.0 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, 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);
|
|
}
|
|
}
|