Compare commits

..
Author SHA1 Message Date
EIREXE 1059b6436b Update gc-adapter-neo to 1.1.0
CI / build-and-test (pull_request) Successful in 1m35s
2026-09-17 17:53:00 +02:00
19 changed files with 61 additions and 246 deletions
+1 -8
View File
@@ -17,14 +17,7 @@
knockback_decay_per_frame: "0.051",
knockback_launch_velocity_multiplier: "0.03",
hitstun_frames_per_unit_knockback: "0.4",
tumble_bounce_hitstop_threshold: 32,
attack_ground_to_airborne_transition_vertical_angle_threshold: "90.0",
meteor_angle_threshold: "10.0",
visual_shake_frames_per_hitstop_frame: "1.2",
visual_shake_table: [
-3.0, -1.5, 1.5, 3.0
],
attacker_visual_shake_multiplier: 0.4
),
stage_manifest_paths: [
"stages/battlefield/test.stage.ron"
+14 -7
View File
@@ -1,12 +1,22 @@
use bevy::prelude::*;
use crate::{
camera::MatchCamera, fighter::{
FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded, animation::FighterAnimationFrame, attack::{
camera::MatchCamera,
fighter::{
FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation,
FighterVelocity, Grounded,
animation::FighterAnimationFrame,
attack::{
FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits,
FighterSolvedHurtboxes, FighterStaleMoveQueue,
}, baked_animation::FighterBoneMatrices, state::FighterState,
}, input::FighterInput, player::Player, stage::line::StageCollision,
},
baked_animation::FighterBoneMatrices,
state::FighterState,
},
input::FighterInput,
player::Player,
stage::line::StageCollision,
};
#[derive(Resource, Default)]
@@ -38,7 +48,6 @@ struct FighterCheckpoint {
damage: FighterDamage,
pending_hit_reaction: FighterPendingHitReaction,
hitstop: FighterHitstop,
hitstop_shake: FighterHitstopShake,
knockback_velocity: FighterKnockbackVelocity,
stale_moves: FighterStaleMoveQueue,
hitboxes: FighterHitboxes,
@@ -137,7 +146,6 @@ pub(super) fn save_or_restore(world: &mut World) {
entity.insert(saved.damage);
entity.insert(saved.pending_hit_reaction.clone());
entity.insert(saved.hitstop);
entity.insert(saved.hitstop_shake);
entity.insert(saved.knockback_velocity);
entity.insert(saved.stale_moves.clone());
entity.insert(saved.hitboxes.clone());
@@ -175,7 +183,6 @@ fn capture_fighter(world: &World, entity: Entity) -> Option<FighterCheckpoint> {
damage: *fighter.get::<FighterDamage>()?,
pending_hit_reaction: fighter.get::<FighterPendingHitReaction>()?.clone(),
hitstop: *fighter.get::<FighterHitstop>()?,
hitstop_shake: *fighter.get::<FighterHitstopShake>()?,
knockback_velocity: *fighter.get::<FighterKnockbackVelocity>()?,
stale_moves: fighter.get::<FighterStaleMoveQueue>()?.clone(),
hitboxes: fighter.get::<FighterHitboxes>()?.clone(),
+1 -9
View File
@@ -43,7 +43,6 @@ pub enum HitReactionKind {
#[derive(Debug, Hash, Clone)]
pub struct FighterHitReaction {
pub launch_velocity: FGVec2,
pub launch_angle: FGi32,
pub hitstop_frames: HitStop,
pub hitstun_frames: u32,
pub kind: HitReactionKind,
@@ -63,13 +62,10 @@ pub struct FighterLastAttackDebug {
pub trajectory_points: Vec<Vec3>,
}
#[derive(Debug, Default, Clone, Deref, DerefMut, Hash, PartialEq, PartialOrd)]
#[derive(Debug, Default, Clone, Deref, DerefMut, Hash)]
pub struct HitStop(u32);
impl HitStop {
pub fn new(hitstop: u32) -> Self {
HitStop(hitstop)
}
pub fn calculate(attack_damage_stalled: FighterDamage, crouch_cancelling: bool) -> Self {
let a = (attack_damage_stalled.0 / FGi32::lit("3.0") + FGi32::lit("4.0")).round_to_zero();
let crouch_cancelling_factor = if crouch_cancelling {
@@ -82,10 +78,6 @@ impl HitStop {
Self(frames)
}
pub fn calculate_shake_frames(&self, game_settings: &GameSettings) -> u32 {
(FGi32::from_num(self.0) * game_settings.fighter_common.visual_shake_frames_per_hitstop_frame).floor().to_num()
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+14 -22
View File
@@ -5,8 +5,15 @@ 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,
Fighter, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterPendingHitReaction, FighterStaleMoveQueue, errors::FighterErrors,
manifest::FighterManifest,
},
game_settings::GameSettings,
math::{int::FGi32, vec::FGVec2},
player::Player,
schedule::GameplaySet,
scripting::FighterAttackScript,
};
pub fn resolve_fighter_hits(
@@ -17,9 +24,8 @@ pub fn resolve_fighter_hits(
&FighterHitboxes,
&Player,
&mut FighterHitstop,
&mut FighterHitstopShake,
)>,
mut receiver: Query<(&mut FighterDamage, &Fighter, &mut FighterPendingHitReaction, &FighterState)>,
mut receiver: Query<(&mut FighterDamage, &Fighter, &mut FighterPendingHitReaction)>,
attack_scripts: Res<Assets<FighterAttackScript>>,
game_settings: Res<GameSettings>,
fighter_manifests: Res<Assets<FighterManifest>>,
@@ -34,7 +40,7 @@ pub fn resolve_fighter_hits(
.position(|hit| hit.defender != defender)
.map_or(hits.len(), |offset| hit_offset + offset);
let Ok((mut receiver_damage, fighter, mut pending_reaction, receiver_state)) = receiver.get_mut(defender)
let Ok((mut receiver_damage, fighter, mut pending_reaction)) = receiver.get_mut(defender)
else {
hit_offset = next_defender;
continue;
@@ -43,7 +49,7 @@ pub fn resolve_fighter_hits(
let damage_data = hits[hit_offset..next_defender]
.iter()
.filter_map(|hit| {
let Ok((stale_move_queue, facing_dir, _hitboxes, player, _, _)) =
let Ok((stale_move_queue, facing_dir, _hitboxes, player, _)) =
attacker.get(hit.attacker)
else {
return None;
@@ -98,7 +104,6 @@ pub fn resolve_fighter_hits(
attacker_slot,
FighterHitReaction {
launch_velocity: launch_direction * knockback.get_launch_speed(&game_settings),
launch_angle,
hitstun_frames: knockback.get_hitstun_frames(&game_settings),
hitstop_frames: hitstop,
kind: HitReactionKind::Flinch,
@@ -109,23 +114,10 @@ pub fn resolve_fighter_hits(
receiver_damage.0 += total_damage_received_this_frame;
for (attacker_entity, _, reaction) in &reactions {
let Ok((.., mut hitstop, mut hitstop_shake)) = attacker.get_mut(*attacker_entity) else {
let Ok((.., mut hitstop)) = attacker.get_mut(*attacker_entity) else {
continue;
};
if hitstop.remaining_frames < reaction.hitstop_frames.0 {
let shake_kind = if receiver_state.is_visually_grounded() {
FighterHitstopShakeKind::Horizontal
} else {
FighterHitstopShakeKind::Vertical
};
hitstop.remaining_frames = reaction.hitstop_frames.0;
let shake_frames = reaction.hitstop_frames.calculate_shake_frames(&game_settings);
hitstop_shake.total_frames = shake_frames;
hitstop_shake.remaining_frames = shake_frames;
hitstop_shake.kind = shake_kind;
hitstop_shake.role = FighterHitstopShakeRole::Attacker;
}
hitstop.remaining_frames = reaction.hitstop_frames.max(hitstop.remaining_frames);
}
pending_reaction.0 = reactions
+17 -32
View File
@@ -33,13 +33,24 @@ pub use motion::{
};
use crate::{
AppState, fighter::{
AppState,
fighter::{
attack::{
AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage,
FighterKnockbackVelocity, FighterPendingHitReaction, FighterSolvedHurtboxes,
FighterStaleMoveQueue,
}, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, manifest::FighterManifest, state::{FighterState, fall::FallState}, visual::FighterAnimations,
}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision,
},
baked_animation::{FighterBoneIndexMap, FighterBoneMatrices},
manifest::FighterManifest,
state::{FighterState, fall::FallState},
visual::FighterAnimations,
},
input::FighterInput,
math::{int::FGi32, vec::FGVec2},
player::Player,
schedule::GameplaySet,
scripting::FighterAttackScript,
stage::line::StageCollision,
};
use state::state_interrupt_system;
@@ -54,30 +65,7 @@ pub enum FighterFacingDirection {
Right,
}
#[derive(Component, Copy, Clone, Hash, Debug, PartialEq, Reflect, Default)]
pub enum FighterHitstopShakeKind {
#[default]
Horizontal,
Vertical
}
#[derive(Component, Copy, Clone, Hash, Debug, PartialEq, Reflect, Default)]
pub enum FighterHitstopShakeRole {
#[default]
Attacker,
Receiver
}
#[derive(Component, Default, Copy, Clone, Hash)]
pub struct FighterHitstopShake {
pub remaining_frames: u32,
pub total_frames: u32,
pub kind: FighterHitstopShakeKind,
pub role: FighterHitstopShakeRole
}
#[derive(Component, Default, Copy, Clone, Hash)]
#[require(FighterHitstopShake)]
pub struct FighterHitstop {
pub remaining_frames: u32,
}
@@ -132,8 +120,7 @@ pub struct FighterVisual;
FighterDamage,
FighterPendingHitReaction,
FighterHitstop,
FighterKnockbackVelocity,
FighterTranslation,
FighterKnockbackVelocity
)]
pub struct Fighter {
pub manifest: Handle<FighterManifest>,
@@ -243,7 +230,6 @@ impl Plugin for FighterPlugin {
.rollback_component_with_clone::<FighterState>()
.rollback_component_with_clone::<FighterHitboxes>()
.rollback_component_with_clone::<FighterBoneMatrices>()
.rollback_component_with_clone::<FighterHitstopShake>()
.checksum_component_with_hash::<FighterHitboxes>()
.rollback_component_with_clone::<FighterSolvedHurtboxes>()
.checksum_component_with_hash::<FighterSolvedHurtboxes>()
@@ -313,9 +299,8 @@ pub fn spawn_fighter(
.id()
}
pub fn decay_hitstop(query: Query<(&mut FighterHitstop, &mut FighterHitstopShake)>) {
for (mut hitstop, mut hitstop_shake) in query {
pub fn decay_hitstop(query: Query<&mut FighterHitstop>) {
for mut hitstop in query {
hitstop.remaining_frames = hitstop.remaining_frames.saturating_sub(1);
hitstop_shake.remaining_frames = hitstop_shake.remaining_frames.saturating_sub(1);
}
}
+9 -28
View File
@@ -5,8 +5,6 @@ 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},
@@ -60,15 +58,13 @@ pub fn apply_fighter_translation_to_visuals(
&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 {
for (translation, previous_translation, ecb, mut transform) in query {
let translation = sample_presentation_translation(
*previous_translation,
*translation,
@@ -81,28 +77,6 @@ pub fn apply_fighter_translation_to_visuals(
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;
}
}
}
@@ -111,15 +85,22 @@ pub fn apply_air_motion(
&FighterVelocity,
&mut FighterTranslation,
&mut FighterPreviousTranslation,
&mut Transform,
)>,
_time: Res<Time<Fixed>>,
) {
for (velocity, mut translation, mut prev_translation) in fighters {
for (velocity, mut translation, mut prev_translation, _transform) in fighters {
prev_translation.0 = translation.0;
translation.0 += velocity.0;
}
}
pub fn copy_fighter_transform_to_visuals(fighters: Query<(&FighterTranslation, &mut Transform)>) {
for (translation, mut transform) in fighters {
transform.translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0);
}
}
pub fn fighter_movement() {}
pub fn snapshot_fighter_motion(
-6
View File
@@ -49,12 +49,6 @@ impl FighterStateImpl for AirDodgeState {
state_context.velocity.0 =
self.direction * state_context.fighter_attributes.air_dodge_velocity;
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
false
}
}
pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
-6
View File
@@ -108,12 +108,6 @@ impl FighterStateImpl for DashState {
) -> Option<FighterState> {
ground::collision_interrupt(state_context, &mut self.ground_common, true)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
fn check_smash_input_with_dir(state_context: &InterruptContext) -> Option<FighterFacingDirection> {
-6
View File
@@ -28,12 +28,6 @@ impl FighterStateImpl for FallState {
crate::fighter::state::land::LandingType::Normal,
)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
false
}
}
pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
-6
View File
@@ -67,12 +67,6 @@ impl FighterStateImpl for GroundAttackState {
) -> Option<FighterState> {
None
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
pub fn check_interrupt(
-9
View File
@@ -95,13 +95,4 @@ impl FighterStateImpl for HitstunState {
}
}
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
match self.hitstop_kind {
HitstopStateKind::Grounded(_) => true,
HitstopStateKind::Airborne => false,
}
}
}
-12
View File
@@ -96,12 +96,6 @@ impl FighterStateImpl for JumpSquatState {
state_context.play_animation(AnimKind::JumpSquat, false);
state_context.input.clear_command(FighterCommands::Jump);
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
impl FighterStateImpl for JumpState {
@@ -180,12 +174,6 @@ impl FighterStateImpl for JumpState {
crate::fighter::state::land::LandingType::Normal,
)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
false
}
}
pub fn check_input(
-6
View File
@@ -69,10 +69,4 @@ impl FighterStateImpl for LandingState {
}
};
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
+5 -44
View File
@@ -7,7 +7,8 @@ use bevy::prelude::*;
use crate::fighter::animation::{AnimKind, FighterAnimationFrame};
use crate::fighter::attack::{
FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction, FighterStaleMoveQueue, HitStop,
FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction,
FighterStaleMoveQueue,
};
use crate::fighter::attributes::FighterAttributes;
use crate::fighter::baked_animation::BakedFighterAnimations;
@@ -16,11 +17,11 @@ use crate::fighter::manifest::FighterManifest;
use crate::fighter::state::hitstun::HitstunState;
use crate::fighter::visual::FighterAnimations;
use crate::fighter::{
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterPreviousTranslation, FighterTranslation, FighterVelocity,
};
use crate::game_settings::GameSettings;
use crate::input::FighterInput;
use crate::math::int::FGi32;
use crate::scripting::FighterAttackScript;
use crate::stage::line::StageCollision;
@@ -65,10 +66,6 @@ macro_rules! fighter_states {
pub fn check_collision_interrupt(&mut self, ctx: &mut CollisionContext) -> Option<FighterState> {
match self { $(Self::$variant(s) => s.check_collision_interrupt(ctx)),* }
}
pub fn is_visually_grounded(&self) -> bool {
match self { $(Self::$variant(s) => s.is_visually_grounded()),* }
}
}
$(impl From<$ty> for FighterState {
@@ -94,10 +91,6 @@ pub trait FighterStateImpl: Sized {
) -> Option<FighterState> {
None
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool;
}
pub struct AnimationStatus {
@@ -327,7 +320,6 @@ pub struct StatefulFighterQuery {
pub attack_script_assets: &'static FighterAttackScriptAssets,
pub pending_hit_reaction: &'static mut FighterPendingHitReaction,
pub hitstop: &'static mut FighterHitstop,
pub hitstop_shake: &'static mut FighterHitstopShake,
}
pub fn state_update_system(
@@ -392,7 +384,6 @@ pub fn state_interrupt_system(
attack_script_assets,
mut pending_hit_reaction,
mut hitstop,
mut hitstop_shake,
} = fighter;
let manifest = fighter_manifests
.get(&fighter.manifest)
@@ -400,17 +391,7 @@ pub fn state_interrupt_system(
let new_state = if let Some(reaction) = pending_hit_reaction.take() {
hitstop.remaining_frames = *reaction.hitstop_frames;
let shake_frames = reaction.hitstop_frames.calculate_shake_frames(&game_settings);
hitstop_shake.role = FighterHitstopShakeRole::Receiver;
hitstop_shake.total_frames = shake_frames;
hitstop_shake.remaining_frames = shake_frames;
hitstop_shake.kind = if state.is_visually_grounded() {
FighterHitstopShakeKind::Horizontal
} else {
FighterHitstopShakeKind::Vertical
};
let mut hitstop_kind = match &*state {
let hitstop_kind = match &*state {
FighterState::Wait(value) => {
hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
}
@@ -431,26 +412,6 @@ pub fn state_interrupt_system(
}
_ => hitstun::HitstopStateKind::Airborne,
};
if let hitstun::HitstopStateKind::Grounded(_) = hitstop_kind {
let straight_up_angle = FGi32::lit("90.0");
if reaction.hitstop_frames >= HitStop::new(game_settings.fighter_common.tumble_bounce_hitstop_threshold) {
hitstop_kind = hitstun::HitstopStateKind::Airborne;
// TODO: Use floor normal here
} else if reaction.launch_angle.abs_diff(straight_up_angle) < game_settings.fighter_common.attack_ground_to_airborne_transition_vertical_angle_threshold {
hitstop_kind = hitstun::HitstopStateKind::Airborne;
} else {
// straight down is 270
let straight_down_angle = FGi32::lit("270.0");
let meteor_threshold = game_settings.fighter_common.meteor_angle_threshold;
if (reaction.launch_angle - straight_down_angle).abs() <= meteor_threshold {
// TODO: Floor bounce
hitstop_kind = hitstun::HitstopStateKind::Airborne;
}
}
}
info!("Entering hitstop kind: {:?}", hitstop_kind);
Some(FighterState::Hitstun(HitstunState {
hit_reaction: reaction,
hitstop_kind,
-6
View File
@@ -64,12 +64,6 @@ impl FighterStateImpl for RunState {
) -> Option<FighterState> {
ground::collision_interrupt(state_context, &mut self.grounded_common, true)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
pub fn check_input(
-6
View File
@@ -56,12 +56,6 @@ impl FighterStateImpl for TurnState {
) -> Option<FighterState> {
ground::collision_interrupt(state_context, &mut self.grounded_common, false)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
pub fn check_input(
-6
View File
@@ -46,12 +46,6 @@ impl FighterStateImpl for WaitState {
) -> Option<FighterState> {
ground::collision_interrupt(state_context, &mut self.grounded_common, false)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
pub fn check_input(
-6
View File
@@ -56,12 +56,6 @@ impl FighterStateImpl for WalkState {
) -> Option<FighterState> {
ground::collision_interrupt(state_context, &mut self.grounded_common, false)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
true
}
}
pub fn check_input(
-21
View File
@@ -9,33 +9,12 @@ use crate::{math::int::FGi32, menus::style::FGUiStyle};
#[reflect(opaque)]
pub struct FighterSettingsCommon {
pub walk_speed_ease: FGi32,
/// Maximum allowed horizontal velocity
pub ground_max_horizontal_velocity: FGi32,
/// Ground friction applied when the fighter is above the walk speed
pub ground_friction_over_walk_speed_multiplier: FGi32,
pub knockback_stacking_window: u32,
pub knockback_decay_per_frame: FGi32,
/// Multiplier that translates knockback -> game unit velocity
pub knockback_launch_velocity_multiplier: FGi32,
pub hitstun_frames_per_unit_knockback: FGi32,
/// If an attack's hitstop > this value it will cause a grounded fighter to either bounce (if a meteor/spike)
/// or to become airborne
pub tumble_bounce_hitstop_threshold: u32,
/// Angle diff from straight down under which the attack will count as a meteor
pub meteor_angle_threshold: FGi32,
/// Angle diff from straight up under which the attack will force an airborne state
pub attack_ground_to_airborne_transition_vertical_angle_threshold: FGi32,
pub visual_shake_frames_per_hitstop_frame: FGi32,
// offsets applied when the fighter is in hitstop
pub visual_shake_table: Vec<f32>,
/// The attacker will always get less hitstop
pub attacker_visual_shake_multiplier: f32,
}
#[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)]