Compare commits

...
Author SHA1 Message Date
EIREXE 540b0cda8d Fix build and clamp DI angle change
CI / build-and-test (pull_request) Successful in 1m15s
2026-09-18 22:35:35 +02:00
EIREXE 158e8b6a66 Implement directional influence 2026-09-18 18:49:20 +02:00
EIREXE 400d8afaad Keeep debug trajectories around 2026-09-18 18:13:02 +02:00
8 changed files with 116 additions and 20 deletions
+2 -1
View File
@@ -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"
@@ -0,0 +1,69 @@
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 angle_change = angle_change.clamp(
-di_max_angle.to_fixed::<FGWide>(),
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};
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,
+3 -5
View File
@@ -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,
+10 -4
View File
@@ -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,14 +382,14 @@ pub fn attack_debug_gather(
&FighterPendingHitReaction,
&FighterTranslation,
&Fighter,
&FighterInput,
)>,
mut commands: Commands,
manifests: Res<Assets<FighterManifest>>,
game_settings: Res<GameSettings>,
) -> Result {
for (entity, reaction, translation, fighter) in query {
for (entity, reaction, translation, fighter, input) in query {
let Some(ref hit) = reaction.0 else {
commands.entity(entity).remove::<FighterLastAttackDebug>();
continue;
};
@@ -397,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);
+24 -6
View File
@@ -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,17 @@ pub enum HitstopStateKind {
pub struct HitstunState {
pub hit_reaction: FighterHitReaction,
pub hitstop_kind: HitstopStateKind,
knockback_applied: bool
}
impl HitstunState {
pub fn new(hit_reaction: FighterHitReaction, hitstop_kind: HitstopStateKind) -> Self {
Self {
hit_reaction,
hitstop_kind,
knockback_applied: false
}
}
}
impl FighterStateImpl for HitstunState {
@@ -59,6 +67,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 +96,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) {}
+3 -3
View File
@@ -451,10 +451,10 @@ pub fn state_interrupt_system(
}
info!("Entering hitstop kind: {:?}", hitstop_kind);
Some(FighterState::Hitstun(HitstunState {
hit_reaction: reaction,
Some(FighterState::Hitstun(HitstunState::new(
reaction,
hitstop_kind,
}))
)))
} else if hitstop.remaining_frames > 0 {
None
} else {
+3
View File
@@ -36,6 +36,9 @@ pub struct FighterSettingsCommon {
pub visual_shake_table: Vec<f32>,
/// 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)]