//! 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::{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); /// Marks a simulation frame whose position changed discontinuously for /// presentation purposes (for example, a landing correction). #[derive(Component, Default, Clone, Copy, Hash, Debug, PartialEq, Eq)] pub struct FighterPresentationSnap(pub bool); #[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 sample_presentation_translation_with_snap( previous: FighterPreviousTranslation, current: FighterTranslation, overstep_fraction: f32, mode: PresentationMotionMode, snapped: FighterPresentationSnap, ) -> Vec2 { if snapped.0 { Vec2::new(current.x.to_num(), current.y.to_num()) } else { sample_presentation_translation(previous, current, overstep_fraction, mode) } } pub fn clear_fighter_presentation_snaps(mut fighters: Query<&mut FighterPresentationSnap>) { for mut snap in &mut fighters { snap.0 = false; } } pub fn apply_fighter_translation_to_visuals( query: Query<( &FighterTranslation, &FighterPreviousTranslation, &FighterPresentationSnap, &FighterHitstopShake, &mut Transform, )>, timing: Res, debug_settings: Res, game_settings: Res, ) { let overstep_fraction = timing.overstep_fraction(); for (translation, previous_translation, presentation_snap, hitstop_shake, mut transform) in query { let translation = sample_presentation_translation_with_snap( *previous_translation, *translation, overstep_fraction, debug_settings.motion_sampling, *presentation_snap, ); transform.translation = Vec3::new( translation.x, translation.y, 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>, ) { 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 presentation_translation_uses_current_sample_after_snap() { let position = sample_presentation_translation_with_snap( FighterPreviousTranslation(FGVec2::new(FGi32::lit("2"), FGi32::lit("4"))), FighterTranslation(FGVec2::new(FGi32::lit("6"), FGi32::lit("12"))), 0.25, PresentationMotionMode::Extrapolation, FighterPresentationSnap(true), ); assert_eq!(position, Vec2::new(6.0, 12.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); } }