Implement directional influence

This commit is contained in:
2026-09-18 18:49:20 +02:00
parent 400d8afaad
commit 158e8b6a66
7 changed files with 91 additions and 16 deletions
+2 -1
View File
@@ -24,7 +24,8 @@
visual_shake_table: [ visual_shake_table: [
-3.0, -1.5, 1.5, 3.0 -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: [ stage_manifest_paths: [
"stages/battlefield/test.stage.ron" "stages/battlefield/test.stage.ron"
@@ -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::<FGWide>();
let (sin, cos) = (attack_angle + angle_change.to_fixed::<FGi32>()).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);
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
mod reaction; mod reaction;
pub mod directional_influence;
use bevy::ecs::entity::{EntityMapper, MapEntities}; use bevy::ecs::entity::{EntityMapper, MapEntities};
use bevy::prelude::*; use bevy::prelude::*;
@@ -42,7 +43,7 @@ pub enum HitReactionKind {
#[derive(Debug, Hash, Clone)] #[derive(Debug, Hash, Clone)]
pub struct FighterHitReaction { pub struct FighterHitReaction {
pub launch_velocity: FGVec2, pub launch_velocity: FGi32,
pub launch_angle: FGi32, pub launch_angle: FGi32,
pub hitstop_frames: HitStop, pub hitstop_frames: HitStop,
pub hitstun_frames: u32, pub hitstun_frames: u32,
+3 -5
View File
@@ -1,12 +1,11 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ggrs::prelude::GgrsSchedule; use bevy_ggrs::prelude::GgrsSchedule;
use fixed_math::SinCos;
use super::*; use super::*;
use crate::{ use crate::{
fighter::{ fighter::{
Fighter, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingHitReaction, FighterStaleMoveQueue, errors::FighterErrors, manifest::FighterManifest, state::FighterState, 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( pub fn resolve_fighter_hits(
@@ -90,14 +89,13 @@ pub fn resolve_fighter_hits(
} else { } else {
angle angle
}; };
let (sin, cos) = launch_angle.sin_cos();
let launch_direction = FGVec2::new(cos, sin);
let hitstop = HitStop::calculate(FighterDamage(attack_damage), false); let hitstop = HitStop::calculate(FighterDamage(attack_damage), false);
reactions.push(( reactions.push((
attacker_entity, attacker_entity,
attacker_slot, attacker_slot,
FighterHitReaction { FighterHitReaction {
launch_velocity: launch_direction * knockback.get_launch_speed(&game_settings), launch_velocity: knockback.get_launch_speed(&game_settings),
launch_angle, launch_angle,
hitstun_frames: knockback.get_hitstun_frames(&game_settings), hitstun_frames: knockback.get_hitstun_frames(&game_settings),
hitstop_frames: hitstop, hitstop_frames: hitstop,
+10 -3
View File
@@ -9,7 +9,7 @@ use bevy_egui::{EguiContexts, egui};
use crate::debug_tools::{DebugSettings, FighterDebugRowKind}; use crate::debug_tools::{DebugSettings, FighterDebugRowKind};
use crate::fighter::animation::FighterAnimationFrame; use crate::fighter::animation::FighterAnimationFrame;
use crate::fighter::attack::{ use crate::fighter::attack::{
FighterKnockbackVelocity, FighterLastAttackDebug, FighterPendingHitReaction, self, FighterKnockbackVelocity, FighterLastAttackDebug, FighterPendingHitReaction,
FighterSolvedHurtboxes, FighterSolvedHurtboxes,
}; };
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices}; use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
@@ -382,12 +382,13 @@ pub fn attack_debug_gather(
&FighterPendingHitReaction, &FighterPendingHitReaction,
&FighterTranslation, &FighterTranslation,
&Fighter, &Fighter,
&FighterInput,
)>, )>,
mut commands: Commands, mut commands: Commands,
manifests: Res<Assets<FighterManifest>>, manifests: Res<Assets<FighterManifest>>,
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
) -> Result { ) -> Result {
for (entity, reaction, translation, fighter) in query { for (entity, reaction, translation, fighter, input) in query {
let Some(ref hit) = reaction.0 else { let Some(ref hit) = reaction.0 else {
continue; continue;
}; };
@@ -396,7 +397,13 @@ pub fn attack_debug_gather(
.get(&fighter.manifest) .get(&fighter.manifest)
.ok_or(FighterErrors::FighterManifestNotFound)?; .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 fighter_vel = FighterVelocity(FGVec2::ZERO);
let mut translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0); let mut translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0);
+14 -6
View File
@@ -1,8 +1,6 @@
use crate::{ use crate::{
fighter::{ fighter::{
animation::AnimKind, animation::AnimKind, attack::{self, FighterHitReaction}, state::{
attack::FighterHitReaction,
state::{
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl,
InterruptContext, UpdateContext, air, InterruptContext, UpdateContext, air,
fall::FallState, fall::FallState,
@@ -10,8 +8,7 @@ use crate::{
land::LandingType, land::LandingType,
wait::WaitState, wait::WaitState,
}, },
}, }, math::int::FGi32,
math::int::FGi32,
}; };
#[derive(Clone, Debug, Hash)] #[derive(Clone, Debug, Hash)]
@@ -24,6 +21,7 @@ pub enum HitstopStateKind {
pub struct HitstunState { pub struct HitstunState {
pub hit_reaction: FighterHitReaction, pub hit_reaction: FighterHitReaction,
pub hitstop_kind: HitstopStateKind, pub hitstop_kind: HitstopStateKind,
knockback_applied: bool
} }
impl FighterStateImpl for HitstunState { impl FighterStateImpl for HitstunState {
@@ -59,6 +57,17 @@ impl FighterStateImpl for HitstunState {
} }
fn update(&mut self, ctx: &mut UpdateContext) { 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); air::integrate_gravity(ctx.velocity, ctx.fighter_attributes);
let speed = ctx.knockback_velocity.length(); let speed = ctx.knockback_velocity.length();
@@ -77,7 +86,6 @@ impl FighterStateImpl for HitstunState {
HitstopStateKind::Airborne => AnimKind::FlinchAir, HitstopStateKind::Airborne => AnimKind::FlinchAir,
}; };
state_context.play_animation(animation, false); 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) {} fn on_exit(&mut self, _state_context: &mut ExitContext) {}
+3
View File
@@ -36,6 +36,9 @@ pub struct FighterSettingsCommon {
pub visual_shake_table: Vec<f32>, pub visual_shake_table: Vec<f32>,
/// The attacker will always get less hitstop /// The attacker will always get less hitstop
pub attacker_visual_shake_multiplier: f32, pub attacker_visual_shake_multiplier: f32,
/// Maximum angle changed caused by directional influence
pub di_max_angle: FGi32,
} }
#[derive(Reflect, Serialize, Deserialize, Clone)] #[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)] #[reflect(opaque)]