Implement hitstun and knockback.

This commit is contained in:
2026-09-13 23:01:25 +02:00
parent 811578af2f
commit 4570860d64
26 changed files with 840 additions and 246 deletions
+2
View File
@@ -5,6 +5,8 @@ use std::path::PathBuf;
#[derive(Parser, Debug, Clone, Resource)] #[derive(Parser, Debug, Clone, Resource)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
pub struct Args { pub struct Args {
#[clap(long)]
pub debug_match: bool,
#[clap(long)] #[clap(long)]
pub synctest: bool, pub synctest: bool,
#[clap(long, default_value_t = 2)] #[clap(long, default_value_t = 2)]
+10 -3
View File
@@ -1,7 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use strum_macros::EnumIter; use strum_macros::EnumIter;
use crate::fighter::{FighterFacingDirection, FighterVisual}; use crate::fighter::{FighterFacingDirection, FighterHitstop, FighterVisual};
/// The simulation-owned animation clock. Rendering may interpolate freely, but /// The simulation-owned animation clock. Rendering may interpolate freely, but
/// gameplay samples baked data only with this rollback-tracked value. /// gameplay samples baked data only with this rollback-tracked value.
@@ -32,10 +32,14 @@ impl FighterAnimationFrame {
} }
} }
pub fn advance_fighter_animation_frames(mut fighters: Query<&mut FighterAnimationFrame>) { pub fn advance_fighter_animation_frames(
for mut animation in &mut fighters { mut fighters: Query<(&mut FighterAnimationFrame, &FighterHitstop)>,
) {
for (mut animation, hitstop) in &mut fighters {
if hitstop.remaining_frames == 0 {
animation.advance(); animation.advance();
} }
}
} }
pub fn apply_animation( pub fn apply_animation(
@@ -81,6 +85,9 @@ pub enum AnimKind {
Fall, Fall,
Turn, Turn,
FlinchGround,
FlinchAir,
// Attacks // Attacks
AttackJab1, AttackJab1,
AttackUpTilt, AttackUpTilt,
+314 -58
View File
@@ -1,37 +1,85 @@
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ggrs::prelude::GgrsSchedule; use bevy_ggrs::{Rollback, prelude::GgrsSchedule};
use fixed_math::SinCos;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
fighter::{ fighter::{
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterRoster, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterRoster,
FighterTranslation, FighterTranslation,
animation::{AnimKind, FighterAnimationFrame}, animation::{AnimKind, FighterAnimationFrame},
baked_animation::{FighterBoneIndexMap, FighterBoneMatrices, FixedMat4}, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices, FixedMat4},
errors::FighterErrors,
hurtbox::{FixedAffineCapsule, FixedCapsule}, hurtbox::{FixedAffineCapsule, FixedCapsule},
manifest::FighterManifest, manifest::FighterManifest,
}, },
game_settings::GameSettings,
math::{ math::{
int::{FGi32, FGi32Ext}, int::{FGWide, FGi32, FGi32Ext},
vec::FGVec2,
vec3::FGVec3, vec3::FGVec3,
}, },
player::Player, player::Player,
schedule::GameplaySet, schedule::GameplaySet,
scripting::FighterAttackScript, scripting::FighterAttackScript,
}; };
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[derive(Deref, Component, Default, Hash, Debug, Copy, Clone)]
pub struct FighterKnockbackVelocity(pub FGVec2);
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash)]
pub struct FighterDamage(FGi32); pub struct FighterDamage(FGi32);
#[derive(Debug, Hash, Clone)]
pub enum HitReactionKind {
Flinch,
Tumble,
}
#[derive(Debug, Hash, Clone)]
pub struct FighterHitReaction {
pub launch_velocity: FGVec2,
pub hitstop_frames: HitStop,
pub hitstun_frames: u32,
pub kind: HitReactionKind,
}
#[derive(Component, DerefMut, Deref, Debug, Default, Hash, Clone)]
pub struct FighterPendingHitReaction(pub Option<FighterHitReaction>);
impl FighterDamage { impl FighterDamage {
pub fn lit(text: &str) -> Self { pub fn lit(text: &str) -> Self {
Self(FGi32::lit(text)) Self(FGi32::lit(text))
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Component)]
pub struct FighterLastAttackDebug {
pub trajectory_points: Vec<Vec3>,
}
#[derive(Debug, Default, Clone, Deref, DerefMut, Hash)]
pub struct HitStop(u32);
impl 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 {
FGi32::lit("0.66666666")
} else {
FGi32::ONE
};
let frames: u32 = ((a * crouch_cancelling_factor).round_to_zero()).to_num();
Self(frames)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum KnockbackType { pub enum KnockbackType {
Normal, Normal,
Fixed, Fixed,
@@ -54,6 +102,22 @@ impl Knockback {
pub fn lit(val: &str) -> Self { pub fn lit(val: &str) -> Self {
Self(FGi32::lit(val)) Self(FGi32::lit(val))
} }
pub fn get_launch_speed(&self, game_settings: &GameSettings) -> FGi32 {
self.0
* game_settings
.fighter_common
.knockback_launch_velocity_multiplier
}
pub fn get_hitstun_frames(&self, game_settings: &GameSettings) -> u32 {
(self.0
* game_settings
.fighter_common
.hitstun_frames_per_unit_knockback)
.floor()
.to_num()
}
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -78,8 +142,9 @@ impl AttackAngle {
} }
} }
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug)] #[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
pub enum AttackKind { pub enum AttackKind {
#[default]
NeutralAir, NeutralAir,
ForwardAir, ForwardAir,
BackAir, BackAir,
@@ -129,7 +194,51 @@ pub struct AttackHitbox {
pub end_frame: u32, pub end_frame: u32,
} }
pub struct StaleMoveQueue(pub VecDeque<AttackKind>); const STALE_MOVE_QUEUE_COUNT: usize = 9;
#[derive(Component, Hash, Clone, Default)]
#[require(Rollback)]
pub struct FighterStaleMoveQueue(VecDeque<AttackKind>);
const STALENESS_REDUCTIONS: [FGi32; 9] = [
FGi32::lit("0.09"),
FGi32::lit("0.08"),
FGi32::lit("0.07"),
FGi32::lit("0.06"),
FGi32::lit("0.05"),
FGi32::lit("0.04"),
FGi32::lit("0.03"),
FGi32::lit("0.02"),
FGi32::lit("0.01"),
];
impl FighterStaleMoveQueue {
pub fn push(&mut self, attack: AttackKind) {
if self.0.len() == STALE_MOVE_QUEUE_COUNT {
self.0.pop_front();
}
self.0.push_back(attack);
}
pub fn get_attack_damage_multiplier(&self, attack: AttackKind) -> FGi32 {
FGi32::ONE
- self
.0
.iter()
.rev()
.enumerate()
.map(|(idx, kind)| {
if *kind == attack {
STALENESS_REDUCTIONS[idx]
} else {
FGi32::ZERO
}
})
.sum::<FGi32>()
.min(FGi32::ONE)
}
}
impl AttackHitbox { impl AttackHitbox {
pub fn validate(&self) -> Result<(), String> { pub fn validate(&self) -> Result<(), String> {
@@ -157,8 +266,6 @@ impl AttackHitbox {
/// Knockback calculation /// Knockback calculation
pub fn calculate_knockback( pub fn calculate_knockback(
&self, &self,
_attack: AttackKind,
receiver_attribs: &FighterAttributes, receiver_attribs: &FighterAttributes,
receiver_damage: FighterDamage, receiver_damage: FighterDamage,
total_damage_received_this_frame: FighterDamage, total_damage_received_this_frame: FighterDamage,
@@ -195,16 +302,23 @@ impl AttackHitbox {
out out
}; };
let s = KB_GROWTH_SCALE * self.knockback_growth; let kb = match self.knockback_type {
let p = receiver_damage.0.floor() + total_damage_received_this_frame.0; KnockbackType::Fixed => FGWide::from_num(self.knockback.0),
let d = attack_damage_unstalled; KnockbackType::Normal => {
let damage_term = p / DAMAGE_TERM_DIVISOR * (d + DAMAGE_BONUS); let p = FGWide::from_num(receiver_damage.0.floor())
+ FGWide::from_num(total_damage_received_this_frame.0);
let d = FGWide::from_num(attack_damage_unstalled);
let s = FGWide::from_num(KB_GROWTH_SCALE * self.knockback_growth);
let damage_term = p / FGWide::from_num(DAMAGE_TERM_DIVISOR)
* (d + FGWide::from_num(DAMAGE_BONUS));
let scaled = damage_term * FGWide::from_num(FGi32::lit("2.0") * BASELINE_WEIGHT)
/ FGWide::from_num(receiver_attribs.weight + BASELINE_WEIGHT);
(scaled * FGWide::from_num(KB_GAIN) + FGWide::from_num(KB_OFFSET)) * s
+ FGWide::from_num(self.knockback.0)
}
};
let scaled = damage_term * (FGi32::lit("2.0") * BASELINE_WEIGHT) Knockback((FGWide::from_num(modifier) * kb).saturating_to_num())
/ (receiver_attribs.weight + BASELINE_WEIGHT);
let kb = (scaled * KB_GAIN + KB_OFFSET) * s + self.knockback.0;
Knockback(modifier * kb)
} }
} }
@@ -232,9 +346,6 @@ pub struct FighterSolvedHitbox {
pub capsule: FixedCapsule, pub capsule: FixedCapsule,
} }
#[derive(Component, Clone, Debug, Default, Hash)]
pub struct FighterSolvedHitboxes(pub Vec<FighterSolvedHitbox>);
pub fn solve_hitboxes( pub fn solve_hitboxes(
mut query: Query<( mut query: Query<(
&FighterBoneMatrices, &FighterBoneMatrices,
@@ -259,21 +370,17 @@ pub fn solve_hitboxes(
animation_frame, animation_frame,
) in &mut query ) in &mut query
{ {
let FighterHitboxes { let Some(active_attack) = hitboxes.0.as_mut() else {
attack_script: script_handle, continue;
active_hitboxes, };
active_hitboxes_solved, active_attack.solved_hitboxes.clear();
..
} = &mut *hitboxes;
active_hitboxes_solved.clear();
if let Some(attack_script) = script_handle {
let attack_script = attack_scripts let attack_script = attack_scripts
.get(attack_script) .get(&active_attack.script)
.expect("Attack script should be valid"); .expect("Attack script should be valid");
let fighter_trf = translation.get_3d_transform(facing_direction); let fighter_trf = translation.get_3d_transform(facing_direction);
let previous_fighter_trf = FighterTranslation(previous_translation.0) let previous_fighter_trf = FighterTranslation(previous_translation.0)
.get_3d_transform(&previous_facing_direction.0); .get_3d_transform(&previous_facing_direction.0);
for hitbox_idx in active_hitboxes.iter().copied() { for hitbox_idx in active_attack.active_hitboxes.iter().copied() {
let hitbox = attack_script let hitbox = attack_script
.hitboxes .hitboxes
.get(hitbox_idx) .get(hitbox_idx)
@@ -295,7 +402,7 @@ pub fn solve_hitboxes(
if hitbox.start_frame == animation_frame.frame { if hitbox.start_frame == animation_frame.frame {
// Sphere only // Sphere only
active_hitboxes_solved.push(FighterSolvedHitbox { active_attack.solved_hitboxes.push(FighterSolvedHitbox {
hitbox_idx, hitbox_idx,
capsule: FixedCapsule { capsule: FixedCapsule {
start: curr_hbox_pos, start: curr_hbox_pos,
@@ -306,7 +413,7 @@ pub fn solve_hitboxes(
} else { } else {
let prev_hbox_matrix = previous_fighter_trf.mul(prev_bone_matrix); let prev_hbox_matrix = previous_fighter_trf.mul(prev_bone_matrix);
let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset); let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset);
active_hitboxes_solved.push(FighterSolvedHitbox { active_attack.solved_hitboxes.push(FighterSolvedHitbox {
hitbox_idx, hitbox_idx,
capsule: FixedCapsule { capsule: FixedCapsule {
start: prev_hbox_pos, start: prev_hbox_pos,
@@ -317,7 +424,6 @@ pub fn solve_hitboxes(
} }
} }
} }
}
} }
#[derive(Component)] #[derive(Component)]
pub struct FighterAttackScriptAssets { pub struct FighterAttackScriptAssets {
@@ -326,6 +432,16 @@ pub struct FighterAttackScriptAssets {
#[derive(Component, Clone, Debug, Default, Hash)] #[derive(Component, Clone, Debug, Default, Hash)]
pub struct FighterSolvedHurtboxes(pub Vec<FighterSolvedHurtbox>); pub struct FighterSolvedHurtboxes(pub Vec<FighterSolvedHurtbox>);
#[derive(Component, Clone, Debug, Default, Hash, DerefMut, Deref)]
pub struct FighterReceivedAttacks(
pub Vec<(
/* attacker */ Entity,
Handle<FighterAttackScript>,
AttackKind,
/* hitbox_idx */ usize,
)>,
);
pub fn solve_hurtboxes( pub fn solve_hurtboxes(
mut query: Query<( mut query: Query<(
&FighterBoneMatrices, &FighterBoneMatrices,
@@ -375,40 +491,50 @@ pub fn solve_hurtboxes(
pub fn intersect_attacks_with_hurtboxes( pub fn intersect_attacks_with_hurtboxes(
roster: Res<FighterRoster>, roster: Res<FighterRoster>,
mut query_attacker: Query<(Entity, &mut FighterHitboxes, &Fighter, &Player)>, mut query_attacker: Query<(
query_attacked: Query<(Entity, &FighterSolvedHurtboxes, &Player)>, Entity,
&mut FighterHitboxes,
&Fighter,
&Player,
&FighterStaleMoveQueue,
)>,
mut query_attacked: Query<(
Entity,
&FighterSolvedHurtboxes,
&Player,
&Fighter,
&mut FighterReceivedAttacks,
)>,
attack_scripts: Res<Assets<FighterAttackScript>>, attack_scripts: Res<Assets<FighterAttackScript>>,
) { ) {
for &entity_hurting in &roster.0 { for &entity_receiving in &roster.0 {
let Ok((_, mut hitboxes, _, _)) = query_attacker.get_mut(entity_hurting) else { let Ok((_, hurtboxes, _, _receiving_fighter, mut received_attacks)) =
query_attacked.get_mut(entity_receiving)
else {
continue; continue;
}; };
if hitboxes.attack_script.is_none() {
continue;
}
if let Some(ref attack_script) = hitboxes.attack_script { for &entity_hurting in &roster.0 {
let attack_script = attack_scripts.get(attack_script);
if attack_script.is_none() {
continue;
}
let attack_script = attack_script.unwrap();
for &entity_receiving in &roster.0 {
if entity_hurting == entity_receiving { if entity_hurting == entity_receiving {
continue; continue;
} }
if hitboxes.hit_fighters.contains(&entity_receiving) { let Ok((_, mut hitboxes, _, _, _)) = query_attacker.get_mut(entity_hurting) else {
continue;
}
let Ok((_, hurtboxes, _)) = query_attacked.get(entity_receiving) else {
continue; continue;
}; };
let Some(active_attack) = hitboxes.0.as_mut() else {
continue;
};
if active_attack.hit_fighters.contains(&entity_receiving) {
continue;
}
let attack_script = attack_scripts
.get(&active_attack.script)
.expect("Attack script should be valid");
let mut hitting_hitbox_idx = None; let mut hitting_hitbox_idx = None;
for hurtbox in hurtboxes.0.iter() { for hurtbox in hurtboxes.0.iter() {
for solved_hitbox in hitboxes.active_hitboxes_solved.iter() { for solved_hitbox in active_attack.solved_hitboxes.iter() {
if hurtbox.capsule.intersects(solved_hitbox.capsule) { if hurtbox.capsule.intersects(solved_hitbox.capsule) {
let candidate = solved_hitbox.hitbox_idx; let candidate = solved_hitbox.hitbox_idx;
if hitting_hitbox_idx.is_none_or(|current: usize| { if hitting_hitbox_idx.is_none_or(|current: usize| {
@@ -425,11 +551,140 @@ pub fn intersect_attacks_with_hurtboxes(
continue; continue;
}; };
hitboxes.hit_fighters.push(entity_receiving); received_attacks.push((
entity_hurting,
active_attack.script.clone(),
active_attack.kind,
hitting_hitbox_idx,
));
info!("Hit by hitbox {}!!!!", hitting_hitbox_idx); info!("Hit by hitbox {}!!!!", hitting_hitbox_idx);
active_attack.hit_fighters.push(entity_receiving);
} }
} }
}
pub fn calculate_received_attacks_trajectories(
mut attacker: Query<(
&mut FighterStaleMoveQueue,
&FighterFacingDirection,
&FighterHitboxes,
&Player,
&mut FighterHitstop,
)>,
receiver: Query<(
&mut FighterReceivedAttacks,
&mut FighterDamage,
&Fighter,
&mut FighterPendingHitReaction,
)>,
attack_scripts: Res<Assets<FighterAttackScript>>,
game_settings: Res<GameSettings>,
fighter_manifests: Res<Assets<FighterManifest>>,
) -> Result {
for (mut received_attacks, mut receiver_damage, fighter, mut pending_reactions) in receiver {
if received_attacks.is_empty() {
continue;
} }
// we got attacked, time to calculate the total damage
let damage_data = received_attacks
.iter()
.filter_map(|(from_entity, attack_script, attack_kind, hitbox_idx)| {
let Ok((stale_move_queue, facing_dir, _hitboxes, player, _attacker_hitstop)) =
attacker.get(*from_entity)
else {
return None;
};
let Some(attack_script) = attack_scripts.get(attack_script) else {
return None;
};
let hitbox = &attack_script.hitboxes[*hitbox_idx];
let damage_received =
stale_move_queue.get_attack_damage_multiplier(*attack_kind) * hitbox.damage.0;
Some((
from_entity,
damage_received,
hitbox,
attack_script,
matches!(facing_dir, FighterFacingDirection::Left),
player.handle,
))
})
.collect::<Vec<_>>();
let total_damage_received_this_frame = damage_data
.iter()
.map(|(_, damage, ..)| damage)
.sum::<FGi32>();
let fighter_manifest = fighter_manifests
.get(&fighter.manifest)
.ok_or(FighterErrors::FighterManifestNotFound)?;
let mut reactions = vec![];
for (attacker_entity, attack_damage, hitbox, _attack_script, facing_left, attacker_slot) in
damage_data
{
let knockback = hitbox.calculate_knockback(
&fighter_manifest.attributes,
*receiver_damage,
FighterDamage(total_damage_received_this_frame),
false,
false,
);
let angle = match &hitbox.angle {
AttackAngle::Normal(angle) => *angle,
AttackAngle::Sakurai => FGi32::lit("32.0"),
};
let launch_angle = if facing_left {
FGi32::lit("180.0") - angle
} 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),
hitstun_frames: knockback.get_hitstun_frames(&game_settings),
hitstop_frames: hitstop,
kind: HitReactionKind::Flinch,
},
));
}
receiver_damage.0 += total_damage_received_this_frame;
for (attacker_entity, _, reaction) in reactions.iter() {
let Ok((.., mut hitstop)) = attacker.get_mut(*attacker_entity) else {
continue;
};
hitstop.remaining_frames = reaction.hitstop_frames.max(hitstop.remaining_frames);
}
// Pick attack by port priority
pending_reactions.0 = reactions
.into_iter()
.min_by(|(_, port_a, _), (_, port_b, _)| port_a.cmp(port_b))
.map(|(_, _, reaction)| reaction);
received_attacks.clear();
}
Ok(())
} }
pub struct FighterAttackPlugin; pub struct FighterAttackPlugin;
@@ -442,10 +697,11 @@ impl Plugin for FighterAttackPlugin {
solve_hurtboxes, solve_hurtboxes,
solve_hitboxes, solve_hitboxes,
intersect_attacks_with_hurtboxes, intersect_attacks_with_hurtboxes,
calculate_received_attacks_trajectories,
) )
.chain() .chain()
.after(crate::fighter::baked_animation::update_fighter_bone_matrices) .after(crate::fighter::baked_animation::update_fighter_bone_matrices)
.in_set(GameplaySet::Animation), .in_set(GameplaySet::Attacks),
); );
} }
} }
+3 -3
View File
@@ -115,9 +115,9 @@ impl FixedMat4 {
/// Replaces the rotation with XYZ Euler angles expressed in radians. /// Replaces the rotation with XYZ Euler angles expressed in radians.
pub fn with_rotation(mut self, rotation: FGVec3) -> Self { pub fn with_rotation(mut self, rotation: FGVec3) -> Self {
let (sin_x, cos_x) = rotation.x.sin_cos(); let (sin_x, cos_x) = rotation.x.sin_cos_rads();
let (sin_y, cos_y) = rotation.y.sin_cos(); let (sin_y, cos_y) = rotation.y.sin_cos_rads();
let (sin_z, cos_z) = rotation.z.sin_cos(); let (sin_z, cos_z) = rotation.z.sin_cos_rads();
self.cols[0][0] = cos_y * cos_z; self.cols[0][0] = cos_y * cos_z;
self.cols[0][1] = cos_x * sin_z + sin_x * sin_y * cos_z; self.cols[0][1] = cos_x * sin_z + sin_x * sin_y * cos_z;
self.cols[0][2] = sin_x * sin_z - cos_x * sin_y * cos_z; self.cols[0][2] = sin_x * sin_z - cos_x * sin_y * cos_z;
+75 -11
View File
@@ -8,20 +8,26 @@ 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::FighterSolvedHurtboxes; use crate::fighter::attack::{
FighterKnockbackVelocity, FighterLastAttackDebug, FighterPendingHitReaction,
FighterSolvedHurtboxes,
};
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices}; use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::errors::FighterErrors;
use crate::fighter::hurtbox::FixedAffineCapsule; use crate::fighter::hurtbox::FixedAffineCapsule;
use crate::fighter::state::StateNameDebug; use crate::fighter::manifest::FighterManifest;
use crate::fighter::state::{FighterState, air};
use crate::fighter::visual::FighterAnimations; use crate::fighter::visual::FighterAnimations;
use crate::fighter::{ use crate::fighter::{
FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation,
FighterTranslation, FighterVelocity, FighterTranslation, FighterVelocity,
}; };
use crate::game_settings::GameSettings;
use crate::input::FighterInput; use crate::input::FighterInput;
use crate::math::int::FGi32; use crate::math::int::FGi32;
use crate::math::vec::FGVec2;
use crate::player::Player; use crate::player::Player;
use crate::scripting::FighterAttackScript;
pub fn debug_draw_ecb( pub fn debug_draw_ecb(
ecbs: Query<( ecbs: Query<(
@@ -51,7 +57,7 @@ pub fn fighter_debug(
&FighterVelocity, &FighterVelocity,
&FighterInput, &FighterInput,
&FighterAnimationFrame, &FighterAnimationFrame,
&StateNameDebug, &FighterState,
&FighterAnimations, &FighterAnimations,
&FighterTranslation, &FighterTranslation,
&FighterFacingDirection, &FighterFacingDirection,
@@ -62,7 +68,7 @@ pub fn fighter_debug(
let mut fighters = query.iter().collect::<Vec<_>>(); let mut fighters = query.iter().collect::<Vec<_>>();
fighters.sort_unstable_by_key(|(player, ..)| player.handle); fighters.sort_unstable_by_key(|(player, ..)| player.handle);
for (player, velocity, input, animation_frame, state_name, animations, translation, facing) in for (player, velocity, input, animation_frame, state, animations, translation, facing) in
fighters fighters
{ {
let Some(rows) = settings.player_rows(player.handle) else { let Some(rows) = settings.player_rows(player.handle) else {
@@ -85,7 +91,7 @@ pub fn fighter_debug(
if let Some(frame_count) = frame_count { if let Some(frame_count) = frame_count {
ui.label(format!( ui.label(format!(
"State: {:?} ({:?} {}/{})", "State: {:?} ({:?} {}/{})",
state_name.0, state.name(),
animation_frame.kind, animation_frame.kind,
animation_frame.frame, animation_frame.frame,
frame_count frame_count
@@ -93,7 +99,8 @@ pub fn fighter_debug(
} else { } else {
ui.label(format!( ui.label(format!(
"State: {:?} ({:?})", "State: {:?} ({:?})",
state_name.0, animation_frame.kind state.name(),
animation_frame.kind
)); ));
} }
} }
@@ -334,12 +341,11 @@ pub fn attack_debug(
&FighterFacingDirection, &FighterFacingDirection,
&FighterAnimationFrame, &FighterAnimationFrame,
)>, )>,
_attack_scripts: Res<Assets<FighterAttackScript>>,
mut gizmos: Gizmos, mut gizmos: Gizmos,
) { ) {
for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query { for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query {
if let Some(_script) = &hitboxes.attack_script { if let Some(active_attack) = &hitboxes.0 {
for solved_hitbox in hitboxes.active_hitboxes_solved.iter() { for solved_hitbox in active_attack.solved_hitboxes.iter() {
let start = solved_hitbox.capsule.start.to_vec3(); let start = solved_hitbox.capsule.start.to_vec3();
let end = solved_hitbox.capsule.end.to_vec3(); let end = solved_hitbox.capsule.end.to_vec3();
let center = let center =
@@ -369,3 +375,61 @@ pub fn attack_debug(
} }
} }
} }
pub fn attack_debug_gather(
query: Query<(
Entity,
&FighterPendingHitReaction,
&FighterTranslation,
&Fighter,
)>,
mut commands: Commands,
manifests: Res<Assets<FighterManifest>>,
game_settings: Res<GameSettings>,
) -> Result {
for (entity, reaction, translation, fighter) in query {
let Some(ref hit) = reaction.0 else {
commands.entity(entity).remove::<FighterLastAttackDebug>();
continue;
};
let manifest = manifests
.get(&fighter.manifest)
.ok_or(FighterErrors::FighterManifestNotFound)?;
let mut knockback_vel = FighterKnockbackVelocity(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);
let mut points = vec![];
points.push(translation);
points.reserve(hit.hitstun_frames as usize);
for _ in 1..hit.hitstun_frames {
air::integrate_gravity(&mut fighter_vel, &manifest.attributes);
let vel = knockback_vel.length();
let new_vel = vel
.saturating_sub(game_settings.fighter_common.knockback_decay_per_frame)
.max(FGi32::ZERO);
knockback_vel = FighterKnockbackVelocity(knockback_vel.0.normalize_or_zero() * new_vel);
translation += Vec3::new(knockback_vel.x.to_num(), knockback_vel.y.to_num(), 0.0);
translation += Vec3::new(fighter_vel.x.to_num(), fighter_vel.y.to_num(), 0.0);
points.push(translation);
}
commands.entity(entity).insert(FighterLastAttackDebug {
trajectory_points: points,
});
}
Ok(())
}
pub fn attack_launch_debug_draw(query: Query<&FighterLastAttackDebug>, mut gizmos: Gizmos) {
for debug_attack in query {
gizmos.linestrip(
debug_attack.trajectory_points.iter().copied(),
bevy::color::palettes::css::GREEN,
);
}
}
+7
View File
@@ -0,0 +1,7 @@
use bevy::reflect::Reflect;
#[derive(thiserror::Error, Debug, Reflect)]
pub enum FighterErrors {
#[error("Fighter manifest not found")]
FighterManifestNotFound,
}
+55 -32
View File
@@ -13,6 +13,7 @@ pub mod baked_animation;
pub mod collision; pub mod collision;
pub mod debug; pub mod debug;
pub mod ecb; pub mod ecb;
pub mod errors;
pub mod hurtbox; pub mod hurtbox;
pub mod manifest; pub mod manifest;
pub mod motion; pub mod motion;
@@ -32,22 +33,17 @@ pub use motion::{
}; };
use crate::{ use crate::{
fighter::{ AppState, fighter::{
attack::{ attack::{
FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHitboxes, AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage,
FighterSolvedHurtboxes, FighterKnockbackVelocity, FighterPendingHitReaction, FighterReceivedAttacks,
FighterSolvedHurtboxes, FighterStaleMoveQueue,
}, },
baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices},
manifest::FighterManifest, manifest::FighterManifest,
state::{FighterState, fall::FallState}, state::{FighterState, fall::FallState},
visual::FighterAnimations, visual::FighterAnimations,
}, }, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision,
input::FighterInput,
math::{int::FGi32, vec::FGVec2},
player::Player,
schedule::GameplaySet,
scripting::FighterAttackScript,
stage::line::StageCollision,
}; };
use state::state_interrupt_system; use state::state_interrupt_system;
@@ -62,6 +58,11 @@ pub enum FighterFacingDirection {
Right, Right,
} }
#[derive(Component, Default, Copy, Clone, Hash)]
pub struct FighterHitstop {
pub remaining_frames: u32,
}
impl FighterFacingDirection { impl FighterFacingDirection {
fn reverse(&self) -> Self { fn reverse(&self) -> Self {
match self { match self {
@@ -107,6 +108,14 @@ impl FighterFacingDirection {
pub struct FighterVisual; pub struct FighterVisual;
#[derive(Component)] #[derive(Component)]
#[require(
FighterReceivedAttacks,
FighterStaleMoveQueue,
FighterDamage,
FighterPendingHitReaction,
FighterHitstop,
FighterKnockbackVelocity
)]
pub struct Fighter { pub struct Fighter {
pub manifest: Handle<FighterManifest>, pub manifest: Handle<FighterManifest>,
} }
@@ -130,7 +139,9 @@ impl Plugin for FighterPlugin {
) )
.add_systems( .add_systems(
GgrsSchedule, GgrsSchedule,
(state::state_update_system,).in_set(GameplaySet::StateUpdate), (state::state_update_system, decay_hitstop)
.chain()
.in_set(GameplaySet::StateUpdate),
) )
/*.add_systems( /*.add_systems(
GgrsSchedule, GgrsSchedule,
@@ -156,6 +167,8 @@ impl Plugin for FighterPlugin {
// is a desync waiting to happen. // is a desync waiting to happen.
.rollback_component_with_copy::<FighterECB>() .rollback_component_with_copy::<FighterECB>()
.checksum_component_with_hash::<FighterECB>() .checksum_component_with_hash::<FighterECB>()
.rollback_component_with_clone::<FighterStaleMoveQueue>()
.checksum_component_with_hash::<FighterStaleMoveQueue>()
.rollback_component_with_copy::<ecb::FighterPreviousECB>() .rollback_component_with_copy::<ecb::FighterPreviousECB>()
.checksum_component_with_hash::<ecb::FighterPreviousECB>() .checksum_component_with_hash::<ecb::FighterPreviousECB>()
.rollback_component_with_copy::<FighterVelocity>() .rollback_component_with_copy::<FighterVelocity>()
@@ -171,6 +184,16 @@ impl Plugin for FighterPlugin {
.rollback_component_with_copy::<Grounded>() .rollback_component_with_copy::<Grounded>()
.checksum_component_with_hash::<Grounded>() .checksum_component_with_hash::<Grounded>()
.rollback_component_with_copy::<FighterFacingDirection>() .rollback_component_with_copy::<FighterFacingDirection>()
.rollback_component_with_copy::<FighterDamage>()
.checksum_component_with_hash::<FighterDamage>()
.rollback_component_with_clone::<FighterPendingHitReaction>()
.checksum_component_with_hash::<FighterPendingHitReaction>()
.rollback_component_with_copy::<FighterHitstop>()
.checksum_component_with_hash::<FighterHitstop>()
.rollback_component_with_copy::<FighterKnockbackVelocity>()
.checksum_component_with_hash::<FighterKnockbackVelocity>()
.rollback_component_with_clone::<FighterReceivedAttacks>()
.checksum_component_with_hash::<FighterReceivedAttacks>()
.rollback_resource_with_reflect::<StageCollision>() .rollback_resource_with_reflect::<StageCollision>()
// Debug views. // Debug views.
.add_systems( .add_systems(
@@ -187,7 +210,13 @@ impl Plugin for FighterPlugin {
) )
.add_systems( .add_systems(
FixedPostUpdate, FixedPostUpdate,
debug::attack_debug.run_if(crate::debug_tools::attack_hitboxes_enabled), (
debug::attack_debug_gather,
(debug::attack_debug, debug::attack_launch_debug_draw)
.after(debug::attack_debug_gather),
)
.chain()
.run_if(crate::debug_tools::attack_hitboxes_enabled).run_if(in_state(AppState::InMatch)),
) )
.add_systems( .add_systems(
FixedPostUpdate, FixedPostUpdate,
@@ -204,23 +233,17 @@ impl Plugin for FighterPlugin {
.register_type::<FighterFacingDirection>(); .register_type::<FighterFacingDirection>();
} }
} }
#[derive(Clone, Debug, Hash)]
#[derive(Component, Clone, Hash)] pub struct FighterActiveAttack {
pub struct FighterHitboxes { pub kind: AttackKind,
pub attack_script: Option<Handle<FighterAttackScript>>, pub script: Handle<FighterAttackScript>,
pub active_hitboxes: Vec<usize>, pub active_hitboxes: Vec<usize>,
pub active_hitboxes_solved: Vec<attack::FighterSolvedHitbox>, pub solved_hitboxes: Vec<attack::FighterSolvedHitbox>,
pub hit_fighters: Vec<Entity>, pub hit_fighters: Vec<Entity>,
} }
impl FighterHitboxes { #[derive(Component, Clone, Debug, Default, Hash)]
pub fn clear(&mut self) { pub struct FighterHitboxes(pub Option<FighterActiveAttack>);
self.attack_script = None;
self.active_hitboxes.clear();
self.active_hitboxes_solved.clear();
self.hit_fighters.clear();
}
}
pub fn spawn_fighter( pub fn spawn_fighter(
commands: &mut Commands, commands: &mut Commands,
@@ -247,12 +270,7 @@ pub fn spawn_fighter(
vertical_half: FGi32::lit("5.0"), vertical_half: FGi32::lit("5.0"),
horizontal_half: FGi32::lit("2.5"), horizontal_half: FGi32::lit("2.5"),
}, },
FighterHitboxes { FighterHitboxes::default(),
attack_script: None,
active_hitboxes: vec![],
active_hitboxes_solved: vec![],
hit_fighters: vec![],
},
), ),
( (
FighterVelocity::default(), FighterVelocity::default(),
@@ -268,7 +286,6 @@ pub fn spawn_fighter(
baked_animation::FighterBoneMatrices::default(), baked_animation::FighterBoneMatrices::default(),
FighterBoneIndexMap::default(), FighterBoneIndexMap::default(),
FighterSolvedHurtboxes::default(), FighterSolvedHurtboxes::default(),
FighterSolvedHitboxes::default(),
FighterVisual, FighterVisual,
FighterInput::default(), FighterInput::default(),
crate::camera::FighterCameraExtents::default(), crate::camera::FighterCameraExtents::default(),
@@ -276,3 +293,9 @@ pub fn spawn_fighter(
)) ))
.id() .id()
} }
pub fn decay_hitstop(query: Query<&mut FighterHitstop>) {
for mut hitstop in query {
hitstop.remaining_frames = hitstop.remaining_frames.saturating_sub(1);
}
}
+4 -4
View File
@@ -18,10 +18,10 @@ pub fn apply_air_drift(
velocity.x += (-velocity_sign) * attribs.air_friction; velocity.x += (-velocity_sign) * attribs.air_friction;
} }
} else { } else {
let target_velocity = let target_velocity = input.get_last_frame().movement.x * attribs.air_drift_max;
input.get_last_frame().movement.x * attribs.air_drift_max; let mut acceleration = input.get_last_frame().movement.x.signum()
let mut acceleration = * attribs.air_acceleration_base
input.get_last_frame().movement.x.signum() * attribs.air_acceleration_base + input.get_last_frame().movement.x * attribs.air_acceleration_stick; + input.get_last_frame().movement.x * attribs.air_acceleration_stick;
// Acceleration opposes velocity // Acceleration opposes velocity
if acceleration * velocity.x < 0 { if acceleration * velocity.x < 0 {
+14 -4
View File
@@ -1,9 +1,16 @@
use crate::{ use crate::{
fighter::{ fighter::{
animation::AnimKind, collision, state::{ animation::AnimKind,
FighterState, FighterStateContext, FighterStateImpl, air, fall::FallState, ground::GroundedStateCommon, land::{LandingState, LandingType}, collision,
state::{
FighterState, FighterStateContext, FighterStateImpl, air,
fall::FallState,
ground::GroundedStateCommon,
land::{LandingState, LandingType},
}, },
}, input, math::vec::FGVec2, },
input,
math::vec::FGVec2,
}; };
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
@@ -42,7 +49,10 @@ impl FighterStateImpl for AirDodgeState {
let grounded_common = GroundedStateCommon { let grounded_common = GroundedStateCommon {
current_line_id: res.line_id, current_line_id: res.line_id,
}; };
Some(FighterState::Land(LandingState { grounded_common, landing_type: LandingType::Special })) Some(FighterState::Land(LandingState {
grounded_common,
landing_type: LandingType::Special,
}))
} else { } else {
None None
} }
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::input::FighterCommands::{SmashMoveLeft, SmashMoveRight};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DashState { pub struct DashState {
direction: FighterFacingDirection, direction: FighterFacingDirection,
ground_common: GroundedStateCommon, pub ground_common: GroundedStateCommon,
} }
impl std::hash::Hash for DashState { impl std::hash::Hash for DashState {
+4 -1
View File
@@ -45,7 +45,10 @@ impl FighterStateImpl for FallState {
let grounded_common = GroundedStateCommon { let grounded_common = GroundedStateCommon {
current_line_id: res.line_id, current_line_id: res.line_id,
}; };
Some(FighterState::Land(LandingState::create(grounded_common, LandingType::Normal))) Some(FighterState::Land(LandingState::create(
grounded_common,
LandingType::Normal,
)))
} else { } else {
None None
} }
+17 -6
View File
@@ -26,14 +26,19 @@ impl FighterStateImpl for GroundAttackState {
} }
fn update(&mut self, state_context: &mut super::FighterStateContext) { fn update(&mut self, state_context: &mut super::FighterStateContext) {
let animation_frame = state_context.get_current_animation_frame();
let script = state_context let script = state_context
.attack_scripts .attack_scripts
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]); .get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
if let Some(script) = script { if let Some(script) = script {
let animation_frame = state_context.get_current_animation_frame();
update_active_hitbox_list( update_active_hitbox_list(
script, script,
&mut state_context.hitboxes.active_hitboxes, &mut state_context
.hitboxes
.0
.as_mut()
.expect("ground attack must have active hitboxes")
.active_hitboxes,
animation_frame, animation_frame,
); );
} }
@@ -41,13 +46,19 @@ impl FighterStateImpl for GroundAttackState {
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) { fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
state_context.play_animation(self.attack_kind.get_animation(), false); state_context.play_animation(self.attack_kind.get_animation(), false);
state_context.hitboxes.attack_script = let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone();
Some(state_context.attack_script_assets.scripts[&self.attack_kind].clone()); state_context.stale_move_queue.push(self.attack_kind);
state_context.hitboxes.active_hitboxes.clear(); state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack {
kind: self.attack_kind,
script,
active_hitboxes: Vec::new(),
solved_hitboxes: Vec::new(),
hit_fighters: Vec::new(),
});
} }
fn on_exit(&mut self, state_context: &mut super::FighterStateContext) { fn on_exit(&mut self, state_context: &mut super::FighterStateContext) {
state_context.hitboxes.clear(); state_context.hitboxes.0 = None;
} }
fn check_collision_interrupt( fn check_collision_interrupt(
+117
View File
@@ -0,0 +1,117 @@
use crate::{
fighter::{
animation::AnimKind,
attack::FighterHitReaction,
collision,
state::{
FighterState, FighterStateContext, FighterStateImpl, air,
fall::FallState,
ground::{GroundedMotionResult, GroundedStateCommon, collide_with_stage_grounded},
land::{LandingState, LandingType},
wait::WaitState,
},
},
math::int::FGi32,
};
#[derive(Clone, Debug, Hash)]
pub enum HitstopStateKind {
Grounded(GroundedStateCommon),
Airborne,
}
#[derive(Clone, Debug, Hash)]
pub struct HitstunState {
pub hit_reaction: FighterHitReaction,
pub hitstop_kind: HitstopStateKind,
}
impl FighterStateImpl for HitstunState {
const NAME: &'static str = "Hitstun";
fn name(&self) -> String {
let hitstop_kind = match self.hitstop_kind {
HitstopStateKind::Grounded(_) => "Grounded",
HitstopStateKind::Airborne => "Airborne",
};
let hit_reaction_type = match &self.hit_reaction.kind {
crate::fighter::attack::HitReactionKind::Flinch => "Flinch",
crate::fighter::attack::HitReactionKind::Tumble => "Tumble",
};
format!("Hitstun ({hitstop_kind}, {hit_reaction_type})")
}
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
if state_context.get_current_animation_frame() > self.hit_reaction.hitstun_frames {
match &self.hitstop_kind {
HitstopStateKind::Grounded(grounded_state_common) => {
Some(FighterState::Wait(WaitState {
grounded_common: grounded_state_common.clone(),
}))
}
HitstopStateKind::Airborne => Some(FighterState::Fall(FallState)),
}
} else {
None
}
}
fn update(&mut self, ctx: &mut FighterStateContext) {
air::integrate_gravity(ctx.velocity, &ctx.fighter_manifest.attributes);
let speed = ctx.knockback_velocity.length();
let next_speed = speed
.saturating_sub(ctx.game_settings.fighter_common.knockback_decay_per_frame)
.max(FGi32::ZERO);
ctx.knockback_velocity.0 = ctx.knockback_velocity.normalize_or_zero() * next_speed;
ctx.translation.0 += ctx.velocity.0 + ctx.knockback_velocity.0;
}
fn on_enter(&mut self, state_context: &mut FighterStateContext) {
let animation = match self.hitstop_kind {
HitstopStateKind::Grounded(_) => AnimKind::FlinchGround,
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 FighterStateContext) {}
fn check_collision_interrupt(
&mut self,
state_context: &mut FighterStateContext,
) -> Option<FighterState> {
match &mut self.hitstop_kind {
HitstopStateKind::Grounded(grounded_state_common) => {
if let GroundedMotionResult::InAir =
collide_with_stage_grounded(state_context, grounded_state_common, true)
{
Some(FighterState::Fall(FallState {}))
} else {
None
}
}
HitstopStateKind::Airborne => {
if let Some(res) = collision::air_collide_with_stage(state_context) {
// Adjust translation
state_context.translation.0 =
res.hit_position - state_context.ecb.get_bottom_point();
let grounded_common = GroundedStateCommon {
current_line_id: res.line_id,
};
Some(FighterState::Land(LandingState::create(
grounded_common,
LandingType::Normal,
)))
} else {
None
}
}
}
}
}
+15 -4
View File
@@ -2,10 +2,18 @@ use bevy::prelude::*;
use crate::{ use crate::{
fighter::{ fighter::{
animation::AnimKind, collision, state::{ animation::AnimKind,
FighterState, FighterStateContext, FighterStateImpl, air, air_dodge::{self, AirDodgeState}, fall::FallState, ground::{self, GroundedMotionResult, GroundedStateCommon}, land::{LandingState, LandingType}, collision,
state::{
FighterState, FighterStateContext, FighterStateImpl, air,
air_dodge::{self, AirDodgeState},
fall::FallState,
ground::{self, GroundedMotionResult, GroundedStateCommon},
land::{LandingState, LandingType},
}, },
}, input::FighterCommands, math::{int::FGi32, vec::FGVec2}, },
input::FighterCommands,
math::{int::FGi32, vec::FGVec2},
}; };
#[derive(Debug, Hash, Clone, Copy)] #[derive(Debug, Hash, Clone, Copy)]
pub enum JumpType { pub enum JumpType {
@@ -231,7 +239,10 @@ impl FighterStateImpl for JumpState {
let grounded_common = GroundedStateCommon { let grounded_common = GroundedStateCommon {
current_line_id: res.line_id, current_line_id: res.line_id,
}; };
Some(FighterState::Land(LandingState::create(grounded_common, LandingType::Normal))) Some(FighterState::Land(LandingState::create(
grounded_common,
LandingType::Normal,
)))
} else { } else {
None None
} }
+9 -5
View File
@@ -14,7 +14,7 @@ use crate::fighter::{
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum LandingType { pub enum LandingType {
Normal, Normal,
Special Special,
} }
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
@@ -25,7 +25,10 @@ pub struct LandingState {
impl LandingState { impl LandingState {
pub fn create(grounded_common: GroundedStateCommon, landing_type: LandingType) -> Self { pub fn create(grounded_common: GroundedStateCommon, landing_type: LandingType) -> Self {
Self { grounded_common, landing_type } Self {
grounded_common,
landing_type,
}
} }
} }
@@ -36,7 +39,8 @@ impl FighterStateImpl for LandingState {
if state_context.is_current_animation_finished() { if state_context.is_current_animation_finished() {
grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common) grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common)
} else if state_context.get_current_animation_frame() } else if state_context.get_current_animation_frame()
>= state_context.fighter_manifest.attributes.landing_iasa && let LandingType::Normal = self.landing_type >= state_context.fighter_manifest.attributes.landing_iasa
&& let LandingType::Normal = self.landing_type
{ {
dash::check_input(state_context, &self.grounded_common).or_else(|| { dash::check_input(state_context, &self.grounded_common).or_else(|| {
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
@@ -88,10 +92,10 @@ impl FighterStateImpl for LandingState {
match self.landing_type { match self.landing_type {
LandingType::Normal => { LandingType::Normal => {
state_context.play_animation(AnimKind::Landing, false); state_context.play_animation(AnimKind::Landing, false);
}, }
LandingType::Special => { LandingType::Special => {
state_context.play_animation(AnimKind::LandingSpecial, false); state_context.play_animation(AnimKind::LandingSpecial, false);
}, }
}; };
} }
} }
+85 -36
View File
@@ -6,14 +6,18 @@ use bevy::ecs::query::QueryData;
use bevy::prelude::*; use bevy::prelude::*;
use crate::fighter::animation::{AnimKind, FighterAnimationFrame}; use crate::fighter::animation::{AnimKind, FighterAnimationFrame};
use crate::fighter::attack::FighterAttackScriptAssets; use crate::fighter::attack::{
FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction,
FighterStaleMoveQueue,
};
use crate::fighter::baked_animation::BakedFighterAnimations; use crate::fighter::baked_animation::BakedFighterAnimations;
use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::manifest::FighterManifest; use crate::fighter::manifest::FighterManifest;
use crate::fighter::state::hitstun::HitstunState;
use crate::fighter::visual::FighterAnimations; use crate::fighter::visual::FighterAnimations;
use crate::fighter::{ use crate::fighter::{
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterTranslation, FighterVelocity, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
}; };
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
use crate::input::FighterInput; use crate::input::FighterInput;
@@ -26,6 +30,7 @@ pub mod dash;
pub mod fall; pub mod fall;
pub mod ground; pub mod ground;
pub mod ground_attack; pub mod ground_attack;
pub mod hitstun;
pub mod jump; pub mod jump;
pub mod land; pub mod land;
pub mod run; pub mod run;
@@ -74,6 +79,9 @@ pub trait FighterStateImpl: Sized {
fn on_enter(&mut self, _state_context: &mut FighterStateContext) {} fn on_enter(&mut self, _state_context: &mut FighterStateContext) {}
fn on_exit(&mut self, _state_context: &mut FighterStateContext) {} fn on_exit(&mut self, _state_context: &mut FighterStateContext) {}
fn name(&self) -> String {
Self::NAME.to_owned()
}
fn update(&mut self, _state_context: &mut FighterStateContext); fn update(&mut self, _state_context: &mut FighterStateContext);
fn check_collision_interrupt( fn check_collision_interrupt(
@@ -90,6 +98,8 @@ pub struct FighterStateContext<'a> {
pub input: &'a mut FighterInput, pub input: &'a mut FighterInput,
pub hitboxes: &'a mut FighterHitboxes, pub hitboxes: &'a mut FighterHitboxes,
pub velocity: &'a mut FighterVelocity, pub velocity: &'a mut FighterVelocity,
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
pub stale_move_queue: &'a mut FighterStaleMoveQueue,
pub ecb: &'a mut FighterECB, pub ecb: &'a mut FighterECB,
pub prev_ecb: &'a mut FighterPreviousECB, pub prev_ecb: &'a mut FighterPreviousECB,
pub facing_direction: &'a mut FighterFacingDirection, pub facing_direction: &'a mut FighterFacingDirection,
@@ -136,6 +146,7 @@ fighter_states! {
AirDodge => air_dodge::AirDodgeState, AirDodge => air_dodge::AirDodgeState,
Turn => turn::TurnState, Turn => turn::TurnState,
GroundAttack => ground_attack::GroundAttackState, GroundAttack => ground_attack::GroundAttackState,
Hitstun => hitstun::HitstunState,
} }
#[derive(QueryData)] #[derive(QueryData)]
@@ -146,6 +157,8 @@ pub struct FighterFrameQuery {
pub input: &'static mut FighterInput, pub input: &'static mut FighterInput,
pub hitboxes: &'static mut FighterHitboxes, pub hitboxes: &'static mut FighterHitboxes,
pub velocity: &'static mut FighterVelocity, pub velocity: &'static mut FighterVelocity,
pub knockback_velocity: &'static mut FighterKnockbackVelocity,
pub stale_move_queue: &'static mut FighterStaleMoveQueue,
pub ecb: &'static mut FighterECB, pub ecb: &'static mut FighterECB,
pub prev_ecb: &'static mut FighterPreviousECB, pub prev_ecb: &'static mut FighterPreviousECB,
pub facing_direction: &'static mut FighterFacingDirection, pub facing_direction: &'static mut FighterFacingDirection,
@@ -170,6 +183,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
input: &mut self.input, input: &mut self.input,
hitboxes: &mut self.hitboxes, hitboxes: &mut self.hitboxes,
velocity: &mut self.velocity, velocity: &mut self.velocity,
knockback_velocity: &mut self.knockback_velocity,
stale_move_queue: &mut self.stale_move_queue,
ecb: &mut self.ecb, ecb: &mut self.ecb,
prev_ecb: &mut self.prev_ecb, prev_ecb: &mut self.prev_ecb,
facing_direction: &mut self.facing_direction, facing_direction: &mut self.facing_direction,
@@ -188,21 +203,13 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
#[derive(QueryData)] #[derive(QueryData)]
#[query_data(mutable)] #[query_data(mutable)]
pub struct StatefulFighterQuery { pub struct StatefulFighterQuery {
pub entity: Entity,
pub state: &'static mut FighterState, pub state: &'static mut FighterState,
pub frame: FighterFrameQuery, pub frame: FighterFrameQuery,
pub animations: &'static FighterAnimations, pub animations: &'static FighterAnimations,
pub fighter: &'static Fighter, pub fighter: &'static Fighter,
pub attack_script_assets: &'static FighterAttackScriptAssets, pub attack_script_assets: &'static FighterAttackScriptAssets,
} pub pending_hit_reaction: &'static mut FighterPendingHitReaction,
pub hitstop: &'static mut FighterHitstop,
#[derive(Component)]
pub struct StateNameDebug(pub &'static str);
impl std::fmt::Display for StateNameDebug {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
} }
pub fn state_update_system( pub fn state_update_system(
@@ -218,9 +225,14 @@ pub fn state_update_system(
mut state, mut state,
mut frame, mut frame,
animations, animations,
hitstop,
.. ..
} = fighter; } = fighter;
if hitstop.remaining_frames > 0 {
continue;
}
let mut update_ctx = frame.context( let mut update_ctx = frame.context(
&game_settings, &game_settings,
&stage_collision, &stage_collision,
@@ -236,6 +248,20 @@ pub fn state_update_system(
} }
} }
fn transition_state(
current: &mut FighterState,
mut next: FighterState,
ctx: &mut FighterStateContext,
) {
let old_name = current.name();
let new_name = next.name();
current.on_exit(ctx);
next.on_enter(ctx);
*current = next;
info!("{} -> {}", old_name, new_name);
}
pub fn state_interrupt_system( pub fn state_interrupt_system(
mut query: Query<StatefulFighterQuery>, mut query: Query<StatefulFighterQuery>,
stage_collision: Res<StageCollision>, stage_collision: Res<StageCollision>,
@@ -243,16 +269,16 @@ pub fn state_interrupt_system(
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
fighter_manifests: Res<Assets<FighterManifest>>, fighter_manifests: Res<Assets<FighterManifest>>,
attack_scripts: Res<Assets<FighterAttackScript>>, attack_scripts: Res<Assets<FighterAttackScript>>,
mut commands: Commands,
) { ) {
for fighter in &mut query { for fighter in &mut query {
let StatefulFighterQueryItem { let StatefulFighterQueryItem {
entity,
mut state, mut state,
mut frame, mut frame,
animations, animations,
fighter, fighter,
attack_script_assets, attack_script_assets,
mut pending_hit_reaction,
mut hitstop,
} = fighter; } = fighter;
let mut state_context = frame.context( let mut state_context = frame.context(
&game_settings, &game_settings,
@@ -265,17 +291,43 @@ pub fn state_interrupt_system(
.get(&fighter.manifest) .get(&fighter.manifest)
.expect("Fighter manifest should be valid"), .expect("Fighter manifest should be valid"),
); );
if let Some(new_state) = state.check_interrupt(&mut state_context) {
let mut new_state = new_state; let new_state = if let Some(reaction) = pending_hit_reaction.take() {
let old_state_name = state.name(); hitstop.remaining_frames = *reaction.hitstop_frames;
let new_state_name = new_state.name(); let hitstop_kind = match &*state {
state.on_exit(&mut state_context); FighterState::Wait(value) => {
new_state.on_enter(&mut state_context); hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
commands
.entity(entity)
.insert((new_state, StateNameDebug(new_state_name)));
info!("State {} -> {}", old_state_name, new_state_name);
} }
FighterState::Walk(value) => {
hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
}
FighterState::Dash(value) => {
hitstun::HitstopStateKind::Grounded(value.ground_common.clone())
}
FighterState::Run(value) => {
hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
}
FighterState::Turn(value) => {
hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
}
FighterState::GroundAttack(value) => {
hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
}
_ => hitstun::HitstopStateKind::Airborne,
};
Some(FighterState::Hitstun(HitstunState {
hit_reaction: reaction,
hitstop_kind,
}))
} else if hitstop.remaining_frames > 0 {
None
} else {
state.check_interrupt(&mut state_context)
};
if let Some(new_state) = new_state {
transition_state(&mut state, new_state, &mut state_context);
};
} }
} }
@@ -286,16 +338,16 @@ pub fn state_collision_interrupt_system(
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
fighter_manifests: Res<Assets<FighterManifest>>, fighter_manifests: Res<Assets<FighterManifest>>,
attack_scripts: Res<Assets<FighterAttackScript>>, attack_scripts: Res<Assets<FighterAttackScript>>,
mut commands: Commands,
) { ) {
for fighter in &mut query { for fighter in &mut query {
let StatefulFighterQueryItem { let StatefulFighterQueryItem {
entity,
mut state, mut state,
mut frame, mut frame,
animations, animations,
fighter, fighter,
attack_script_assets, attack_script_assets,
hitstop,
..
} = fighter; } = fighter;
let mut state_context = frame.context( let mut state_context = frame.context(
&game_settings, &game_settings,
@@ -308,16 +360,13 @@ pub fn state_collision_interrupt_system(
.get(&fighter.manifest) .get(&fighter.manifest)
.expect("Fighter manifest should be valid"), .expect("Fighter manifest should be valid"),
); );
if hitstop.remaining_frames > 0 {
continue;
}
if let Some(new_state) = state.check_collision_interrupt(&mut state_context) { if let Some(new_state) = state.check_collision_interrupt(&mut state_context) {
let mut new_state = new_state; transition_state(&mut state, new_state, &mut state_context);
let old_state_name = state.name();
let new_state_name = new_state.name();
state.on_exit(&mut state_context);
new_state.on_enter(&mut state_context);
commands
.entity(entity)
.insert((new_state, StateNameDebug(new_state_name)));
info!("State {} -> {}", old_state_name, new_state_name);
} }
} }
} }
+13 -4
View File
@@ -7,14 +7,19 @@ use crate::input::FighterCommands;
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct TurnState { pub struct TurnState {
grounded_common: GroundedStateCommon, pub grounded_common: GroundedStateCommon,
} }
impl FighterStateImpl for TurnState { impl FighterStateImpl for TurnState {
const NAME: &'static str = "Turn"; const NAME: &'static str = "Turn";
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> { fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
// State chain that results in the turn completing // State chain that results in the turn completing
if state_context.get_current_animation_frame() <= state_context.game_settings.input_common.turn_into_dashback_window as u32 { if state_context.get_current_animation_frame()
<= state_context
.game_settings
.input_common
.turn_into_dashback_window as u32
{
if let Some(state) = dash::check_input(state_context, &self.grounded_common) { if let Some(state) = dash::check_input(state_context, &self.grounded_common) {
return Some(state); return Some(state);
} }
@@ -86,13 +91,17 @@ pub fn check_input(
) -> Option<FighterState> { ) -> Option<FighterState> {
let input_frame = state_context.input.get_last_frame(); let input_frame = state_context.input.get_last_frame();
let has_smash_input = state_context.input.has_command(FighterCommands::SmashMoveLeft) || state_context.input.has_command(FighterCommands::SmashMoveRight); let has_smash_input = state_context
.input
.has_command(FighterCommands::SmashMoveLeft)
|| state_context
.input
.has_command(FighterCommands::SmashMoveRight);
if !input_frame.movement.x.is_zero() if !input_frame.movement.x.is_zero()
&& input_frame.movement.x.signum() != state_context.facing_direction.to_sign() && input_frame.movement.x.signum() != state_context.facing_direction.to_sign()
&& !has_smash_input && !has_smash_input
{ {
dbg!(input_frame.movement.x);
return Some(FighterState::Turn(TurnState { return Some(FighterState::Turn(TurnState {
grounded_common: grounded_common.clone(), grounded_common: grounded_common.clone(),
})); }));
-1
View File
@@ -3,7 +3,6 @@ use crate::fighter::animation::AnimKind;
use crate::fighter::state::FighterState; use crate::fighter::state::FighterState;
use crate::fighter::state::fall::FallState; use crate::fighter::state::fall::FallState;
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
use crate::fighter::state::turn;
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct WalkState { pub struct WalkState {
+4
View File
@@ -11,6 +11,10 @@ pub struct FighterSettingsCommon {
pub walk_speed_ease: FGi32, pub walk_speed_ease: FGi32,
pub ground_max_horizontal_velocity: FGi32, pub ground_max_horizontal_velocity: FGi32,
pub ground_friction_over_walk_speed_multiplier: FGi32, pub ground_friction_over_walk_speed_multiplier: FGi32,
pub knockback_stacking_window: u32,
pub knockback_decay_per_frame: FGi32,
pub knockback_launch_velocity_multiplier: FGi32,
pub hitstun_frames_per_unit_knockback: FGi32,
} }
#[derive(Reflect, Serialize, Deserialize, Clone)] #[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)] #[reflect(opaque)]
+4 -4
View File
@@ -171,10 +171,10 @@ pub fn gc_event_system(
} }
}; };
} }
digital!(a, GamepadButton::South); digital!(a, GamepadButton::East);
digital!(b, GamepadButton::West); digital!(b, GamepadButton::South);
digital!(x, GamepadButton::East); digital!(x, GamepadButton::North);
digital!(y, GamepadButton::North); digital!(y, GamepadButton::West);
digital!(z, GamepadButton::Z); digital!(z, GamepadButton::Z);
digital!(start, GamepadButton::Start); digital!(start, GamepadButton::Start);
digital!(dpad_up, GamepadButton::DPadUp); digital!(dpad_up, GamepadButton::DPadUp);
+1 -1
View File
@@ -272,7 +272,7 @@ fn read_local_inputs(
Some((_, LocalInputSource::Gamepad(_))) => gamepad Some((_, LocalInputSource::Gamepad(_))) => gamepad
.map(|pad| gamepad::sample_gamepad(pad, &input_map)) .map(|pad| gamepad::sample_gamepad(pad, &input_map))
.unwrap_or_default(), .unwrap_or_default(),
None => FighterInputFrame::default() None => FighterInputFrame::default(),
}; };
// preprocess frame // preprocess frame
+15 -4
View File
@@ -8,7 +8,9 @@ use bevy_ggrs::prelude::Session;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
AppState, args::Args, fighter::{ AppState,
args::Args,
fighter::{
FighterId, FighterRoster, FighterId, FighterRoster,
animation::AnimKind, animation::AnimKind,
attack::{AttackKind, FighterAttackScriptAssets}, attack::{AttackKind, FighterAttackScriptAssets},
@@ -16,7 +18,12 @@ use crate::{
manifest::{FighterManifest, FighterManifestRegistry}, manifest::{FighterManifest, FighterManifestRegistry},
spawn_fighter, spawn_fighter,
visual::FighterAnimations, visual::FighterAnimations,
}, input::{LocalInputAssignments, LocalInputSource}, math::{int::FGi32, vec::FGVec2}, netcode::{GGRSCfg, session::create_session}, scripting::FighterAttackScript, stage::{ },
input::{LocalInputAssignments, LocalInputSource},
math::{int::FGi32, vec::FGVec2},
netcode::{GGRSCfg, session::create_session},
scripting::FighterAttackScript,
stage::{
self, self,
manifest::{StageId, StageManifest, StageManifestRegistry}, manifest::{StageId, StageManifest, StageManifestRegistry},
}, },
@@ -145,6 +152,10 @@ pub fn initiate_default_match(
mut next_state: ResMut<NextState<AppState>>, mut next_state: ResMut<NextState<AppState>>,
completed: Option<Res<crate::replay::ReplayComplete>>, completed: Option<Res<crate::replay::ReplayComplete>>,
) { ) {
if !args.debug_match {
next_state.set(AppState::MainMenu);
return;
}
if completed.is_some() { if completed.is_some() {
return; return;
} }
@@ -174,8 +185,8 @@ pub fn initiate_default_match(
assigments.push((0, LocalInputSource::Keyboard)); assigments.push((0, LocalInputSource::Keyboard));
for (idx, entity) in gamepads.iter().take(args.players-1).enumerate() { for (idx, entity) in gamepads.iter().take(args.players - 1).enumerate() {
assigments.push((idx+1, LocalInputSource::Gamepad(entity))); assigments.push((idx + 1, LocalInputSource::Gamepad(entity)));
} }
initiate_match( initiate_match(
+2 -2
View File
@@ -4,7 +4,7 @@ pub type FGWide = fixed::FixedI128<fixed::types::extra::U32>;
/// Angle conversions for the game's Q16.16 fixed-point values. /// Angle conversions for the game's Q16.16 fixed-point values.
pub trait FGi32Ext { pub trait FGi32Ext {
fn to_radians(self) -> FGi32; fn to_radians(self) -> FGi32;
fn sin_cos(self) -> (FGi32, FGi32); fn sin_cos_rads(self) -> (FGi32, FGi32);
} }
impl FGi32Ext for FGi32 { impl FGi32Ext for FGi32 {
@@ -12,7 +12,7 @@ impl FGi32Ext for FGi32 {
FGi32::from_num(FGWide::from_num(self) * FGWide::lit("0.01745329251994329576923690768489")) FGi32::from_num(FGWide::from_num(self) * FGWide::lit("0.01745329251994329576923690768489"))
} }
fn sin_cos(self) -> (FGi32, FGi32) { fn sin_cos_rads(self) -> (FGi32, FGi32) {
const PI: FGi32 = FGi32::lit("3.14159265358979323846"); const PI: FGi32 = FGi32::lit("3.14159265358979323846");
const HALF_PI: FGi32 = FGi32::lit("1.57079632679489661923"); const HALF_PI: FGi32 = FGi32::lit("1.57079632679489661923");
const TWO_PI: FGi32 = FGi32::lit("6.28318530717958647692"); const TWO_PI: FGi32 = FGi32::lit("6.28318530717958647692");
-3
View File
@@ -130,9 +130,6 @@ impl FGSegment2d {
let t = (s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / denom; let t = (s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / denom;
if s >= FGWide::ZERO && s <= FGWide::ONE && t >= FGWide::ZERO && t <= FGWide::ONE { if s >= FGWide::ZERO && s <= FGWide::ONE && t >= FGWide::ZERO && t <= FGWide::ONE {
if self.left_normal().dot(rhs.point2() - rhs.point1()) >= FGi32::ZERO {
return None;
}
Some(FGVec2::new( Some(FGVec2::new(
FGi32::from_num(p0_x + t * s1_x), FGi32::from_num(p0_x + t * s1_x),
FGi32::from_num(p0_y + t * s1_y), FGi32::from_num(p0_y + t * s1_y),
+3
View File
@@ -23,6 +23,8 @@ pub enum GameplaySet {
Animation, Animation,
/// Resolve the integrated positions against the stage. /// Resolve the integrated positions against the stage.
Collision, Collision,
/// Resolve attacks
Attacks,
} }
/// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by /// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by
@@ -40,6 +42,7 @@ impl Plugin for GameplaySchedulePlugin {
GameplaySet::Physics, GameplaySet::Physics,
GameplaySet::Collision, GameplaySet::Collision,
GameplaySet::Animation, GameplaySet::Animation,
GameplaySet::Attacks,
) )
.chain(), .chain(),
); );
+7
View File
@@ -58,6 +58,13 @@ impl StagePoly {
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(segment_idx, segment)| { .filter_map(|(segment_idx, segment)| {
if segment
.normal
.dot(ray_segment.point2() - ray_segment.point1())
>= crate::math::int::FGWide::ZERO
{
return None;
}
segment segment
.segment .segment
.segment_intersection(&ray_segment) .segment_intersection(&ray_segment)