diff --git a/assets/game_settings.ron b/assets/game_settings.ron index c691518..e9eecc8 100644 --- a/assets/game_settings.ron +++ b/assets/game_settings.ron @@ -24,7 +24,8 @@ visual_shake_table: [ -3.0, -1.5, 1.5, 3.0 ], - attacker_visual_shake_multiplier: 0.4 + attacker_visual_shake_multiplier: 0.4, + di_max_angle: "18.0" ), stage_manifest_paths: [ "stages/battlefield/test.stage.ron" diff --git a/src/fighter/attack/directional_influence.rs b/src/fighter/attack/directional_influence.rs new file mode 100644 index 0000000..13978e9 --- /dev/null +++ b/src/fighter/attack/directional_influence.rs @@ -0,0 +1,57 @@ +use fixed::traits::ToFixed; +use fixed_math::SinCos; + +use crate::math::{ + int::{FGWide, FGi32}, + vec::FGVec2, +}; + +/// Applies DI to a given attack direction +pub fn apply_directional_influence( + receiver_stick: FGVec2, + attack_angle: FGi32, + di_max_angle: FGi32, +) -> FGVec2 { + let (sin, cos) = attack_angle.sin_cos(); + let base_launch_direction = FGVec2::new(cos, sin); + + let Some(base_launch_direction) = base_launch_direction.normalize() else { + return FGVec2::ZERO; + }; + + if receiver_stick.length_squared().is_zero() { + return base_launch_direction; + } + + let normal = FGVec2::new(-base_launch_direction.y, base_launch_direction.x); + let di_amount = receiver_stick.dot(normal); + let angle_change = di_amount * di_max_angle.to_fixed::(); + let (sin, cos) = (attack_angle + angle_change.to_fixed::()).sin_cos(); + FGVec2::new(cos, sin).normalize_or_zero() +} + +#[cfg(test)] +mod test { +use crate::{ + fighter::attack::directional_influence::apply_directional_influence, + math::{int::FGi32, vec::FGVec2}, + }; + + #[test] + fn di_zero_passes_unmodified() { + let di = apply_directional_influence(FGVec2::ZERO, FGi32::lit("0.0"), FGi32::lit("18.0")); + const EPSILON: FGi32 = FGi32::from_bits(4); + assert!(di.x.abs_diff(FGi32::ONE) < EPSILON); + assert!(di.y.abs_diff(FGi32::ZERO) < EPSILON); + } + + #[test] + fn di_applies_on_the_correct_side() { + let di = apply_directional_influence(FGVec2::lit("0.0", "1.0"), FGi32::lit("0.0"), FGi32::lit("18.0")); + assert!(di.x > FGi32::ZERO); + assert!(di.y > FGi32::ZERO); + let di = apply_directional_influence(FGVec2::lit("0.0", "-1.0"), FGi32::lit("0.0"), FGi32::lit("18.0")); + assert!(di.x > FGi32::ZERO); + assert!(di.y < FGi32::ZERO); + } +} diff --git a/src/fighter/attack/mod.rs b/src/fighter/attack/mod.rs index 8018a25..c9f6ef9 100644 --- a/src/fighter/attack/mod.rs +++ b/src/fighter/attack/mod.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, VecDeque}; mod reaction; +pub mod directional_influence; use bevy::ecs::entity::{EntityMapper, MapEntities}; use bevy::prelude::*; @@ -42,7 +43,7 @@ pub enum HitReactionKind { #[derive(Debug, Hash, Clone)] pub struct FighterHitReaction { - pub launch_velocity: FGVec2, + pub launch_velocity: FGi32, pub launch_angle: FGi32, pub hitstop_frames: HitStop, pub hitstun_frames: u32, diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index a6f0504..f9105f2 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -1,12 +1,11 @@ use bevy::prelude::*; use bevy_ggrs::prelude::GgrsSchedule; -use fixed_math::SinCos; use super::*; use crate::{ fighter::{ Fighter, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingHitReaction, FighterStaleMoveQueue, errors::FighterErrors, manifest::FighterManifest, state::FighterState, - }, game_settings::GameSettings, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, + }, game_settings::GameSettings, math::{int::FGi32}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, }; pub fn resolve_fighter_hits( @@ -90,14 +89,13 @@ pub fn resolve_fighter_hits( } else { angle }; - let (sin, cos) = launch_angle.sin_cos(); - let launch_direction = FGVec2::new(cos, sin); + let hitstop = HitStop::calculate(FighterDamage(attack_damage), false); reactions.push(( attacker_entity, attacker_slot, FighterHitReaction { - launch_velocity: launch_direction * knockback.get_launch_speed(&game_settings), + launch_velocity: knockback.get_launch_speed(&game_settings), launch_angle, hitstun_frames: knockback.get_hitstun_frames(&game_settings), hitstop_frames: hitstop, diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index 698b79e..2713d50 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -9,7 +9,7 @@ use bevy_egui::{EguiContexts, egui}; use crate::debug_tools::{DebugSettings, FighterDebugRowKind}; use crate::fighter::animation::FighterAnimationFrame; use crate::fighter::attack::{ - FighterKnockbackVelocity, FighterLastAttackDebug, FighterPendingHitReaction, + self, FighterKnockbackVelocity, FighterLastAttackDebug, FighterPendingHitReaction, FighterSolvedHurtboxes, }; use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices}; @@ -382,12 +382,13 @@ pub fn attack_debug_gather( &FighterPendingHitReaction, &FighterTranslation, &Fighter, + &FighterInput, )>, mut commands: Commands, manifests: Res>, game_settings: Res, ) -> Result { - for (entity, reaction, translation, fighter) in query { + for (entity, reaction, translation, fighter, input) in query { let Some(ref hit) = reaction.0 else { continue; }; @@ -396,7 +397,13 @@ pub fn attack_debug_gather( .get(&fighter.manifest) .ok_or(FighterErrors::FighterManifestNotFound)?; - let mut knockback_vel = FighterKnockbackVelocity(hit.launch_velocity); + let launch_dir = attack::directional_influence::apply_directional_influence( + input.get_last_frame().movement, + hit.launch_angle, + game_settings.fighter_common.di_max_angle, + ); + + let mut knockback_vel = FighterKnockbackVelocity(launch_dir * hit.launch_velocity); let mut fighter_vel = FighterVelocity(FGVec2::ZERO); let mut translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0); diff --git a/src/fighter/state/hitstun.rs b/src/fighter/state/hitstun.rs index ad1dd35..558cba8 100644 --- a/src/fighter/state/hitstun.rs +++ b/src/fighter/state/hitstun.rs @@ -1,8 +1,6 @@ use crate::{ fighter::{ - animation::AnimKind, - attack::FighterHitReaction, - state::{ + animation::AnimKind, attack::{self, FighterHitReaction}, state::{ CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air, fall::FallState, @@ -10,8 +8,7 @@ use crate::{ land::LandingType, wait::WaitState, }, - }, - math::int::FGi32, + }, math::int::FGi32, }; #[derive(Clone, Debug, Hash)] @@ -24,6 +21,7 @@ pub enum HitstopStateKind { pub struct HitstunState { pub hit_reaction: FighterHitReaction, pub hitstop_kind: HitstopStateKind, + knockback_applied: bool } impl FighterStateImpl for HitstunState { @@ -59,6 +57,17 @@ impl FighterStateImpl for HitstunState { } fn update(&mut self, ctx: &mut UpdateContext) { + if !self.knockback_applied { + let launch_direction = attack::directional_influence::apply_directional_influence( + ctx.input.get_last_frame().movement, + self.hit_reaction.launch_angle, + ctx.game_settings.fighter_common.di_max_angle + ); + + ctx.knockback_velocity.0 = launch_direction * self.hit_reaction.launch_velocity; + self.knockback_applied = true; + } + air::integrate_gravity(ctx.velocity, ctx.fighter_attributes); let speed = ctx.knockback_velocity.length(); @@ -77,7 +86,6 @@ impl FighterStateImpl for HitstunState { HitstopStateKind::Airborne => AnimKind::FlinchAir, }; state_context.play_animation(animation, false); - state_context.knockback_velocity.0 = self.hit_reaction.launch_velocity; } fn on_exit(&mut self, _state_context: &mut ExitContext) {} diff --git a/src/game_settings.rs b/src/game_settings.rs index 568c30b..c1ae491 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -36,6 +36,9 @@ pub struct FighterSettingsCommon { pub visual_shake_table: Vec, /// The attacker will always get less hitstop pub attacker_visual_shake_multiplier: f32, + + /// Maximum angle changed caused by directional influence + pub di_max_angle: FGi32, } #[derive(Reflect, Serialize, Deserialize, Clone)] #[reflect(opaque)]