CI / build-and-test (push) Successful in 1m30s
Reviewed-on: #45 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
613 lines
19 KiB
Rust
613 lines
19 KiB
Rust
use std::collections::{HashMap, VecDeque};
|
|
|
|
mod reaction;
|
|
pub mod directional_influence;
|
|
|
|
use bevy::ecs::entity::{EntityMapper, MapEntities};
|
|
use bevy::prelude::*;
|
|
use bevy_ggrs::{Rollback, RollbackApp, prelude::GgrsSchedule};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{
|
|
fighter::{
|
|
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes,
|
|
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterRoster,
|
|
FighterTranslation,
|
|
animation::{AnimKind, FighterAnimationFrame},
|
|
baked_animation::{FighterBoneIndexMap, FighterBoneMatrices, FixedMat4},
|
|
hurtbox::{FixedAffineCapsule, FixedCapsule},
|
|
manifest::FighterManifest,
|
|
},
|
|
game_settings::GameSettings,
|
|
math::{
|
|
int::{FGWide, FGi32, FGi32Ext},
|
|
vec::FGVec2,
|
|
vec3::FGVec3,
|
|
},
|
|
player::Player,
|
|
schedule::GameplaySet,
|
|
scripting::FighterAttackScript,
|
|
};
|
|
|
|
#[derive(Deref, Component, Default, Hash, Debug, Copy, Clone)]
|
|
pub struct FighterKnockbackVelocity(pub FGVec2);
|
|
|
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash, PartialEq)]
|
|
pub struct FighterDamage(FGi32);
|
|
|
|
impl std::fmt::Display for FighterDamage {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{:.2}", self.0)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Hash, Clone)]
|
|
pub enum HitReactionKind {
|
|
Flinch,
|
|
Tumble,
|
|
}
|
|
|
|
#[derive(Debug, Hash, Clone)]
|
|
pub struct FighterHitReaction {
|
|
pub launch_velocity: FGi32,
|
|
pub launch_angle: FGi32,
|
|
pub hitstop_frames: HitStop,
|
|
pub hitstun_frames: u32,
|
|
pub kind: HitReactionKind,
|
|
pub damage: FighterDamage,
|
|
}
|
|
|
|
#[derive(Component, DerefMut, Deref, Debug, Default, Hash, Clone)]
|
|
pub struct FighterPendingHitReaction(pub Option<FighterHitReaction>);
|
|
|
|
impl FighterDamage {
|
|
pub fn lit(text: &str) -> Self {
|
|
Self(FGi32::lit(text))
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct FighterLastAttackDebug {
|
|
pub trajectory_points: Vec<Vec3>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Deref, DerefMut, Hash, PartialEq, PartialOrd)]
|
|
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 {
|
|
FGi32::lit("0.66666666")
|
|
} else {
|
|
FGi32::ONE
|
|
};
|
|
|
|
let frames: u32 = ((a * crouch_cancelling_factor).round_to_zero()).to_num();
|
|
|
|
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)]
|
|
pub enum KnockbackType {
|
|
Normal,
|
|
Fixed,
|
|
}
|
|
|
|
impl KnockbackType {
|
|
pub fn make_normal() -> Self {
|
|
Self::Normal
|
|
}
|
|
|
|
pub fn make_fixed() -> Self {
|
|
Self::Fixed
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct Knockback(FGi32);
|
|
|
|
impl Knockback {
|
|
pub fn lit(val: &str) -> Self {
|
|
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)]
|
|
pub enum AttackAngle {
|
|
Normal(FGi32),
|
|
Sakurai,
|
|
}
|
|
|
|
impl Default for AttackAngle {
|
|
fn default() -> Self {
|
|
AttackAngle::Normal(FGi32::lit("0"))
|
|
}
|
|
}
|
|
|
|
impl AttackAngle {
|
|
pub fn make_normal(angle: FGi32) -> Self {
|
|
Self::Normal(angle)
|
|
}
|
|
|
|
pub fn make_sakurai() -> Self {
|
|
Self::Sakurai
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
|
|
pub enum AttackKind {
|
|
#[default]
|
|
NeutralAir,
|
|
ForwardAir,
|
|
BackAir,
|
|
DownAir,
|
|
UpAir,
|
|
Jab,
|
|
UpTilt,
|
|
DownTilt,
|
|
ForwardTilt,
|
|
UpSmash,
|
|
DownSmash,
|
|
ForwardSmash,
|
|
}
|
|
|
|
impl AttackKind {
|
|
pub fn get_animation(&self) -> AnimKind {
|
|
match self {
|
|
AttackKind::NeutralAir => AnimKind::AttackJab1,
|
|
AttackKind::Jab => AnimKind::AttackJab1,
|
|
AttackKind::UpTilt => AnimKind::AttackUpTilt,
|
|
AttackKind::ForwardAir => AnimKind::AttackUpTilt,
|
|
AttackKind::BackAir => AnimKind::AttackUpTilt,
|
|
AttackKind::DownAir => AnimKind::AttackUpTilt,
|
|
AttackKind::UpAir => AnimKind::AttackUpTilt,
|
|
AttackKind::DownTilt => AnimKind::AttackDownTilt,
|
|
AttackKind::ForwardTilt => AnimKind::AttackForwardTilt,
|
|
AttackKind::UpSmash => AnimKind::AttackUpTilt,
|
|
AttackKind::DownSmash => AnimKind::AttackUpTilt,
|
|
AttackKind::ForwardSmash => AnimKind::AttackUpTilt,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
|
|
#[reflect(opaque)]
|
|
pub struct AttackHitbox {
|
|
pub id: u32,
|
|
pub bone: String,
|
|
pub damage: FighterDamage,
|
|
pub offset: FGVec3,
|
|
pub radius: FGi32,
|
|
pub angle: AttackAngle,
|
|
pub knockback_type: KnockbackType,
|
|
pub knockback: Knockback,
|
|
pub knockback_growth: FGi32,
|
|
pub start_frame: u32,
|
|
pub end_frame: u32,
|
|
}
|
|
|
|
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 {
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
if self.bone.trim().is_empty() {
|
|
return Err("bone name is empty".into());
|
|
}
|
|
if self.radius <= FGi32::ZERO {
|
|
return Err("radius must be positive".into());
|
|
}
|
|
if self.damage.0 < FGi32::ZERO {
|
|
return Err("damage must be non-negative".into());
|
|
}
|
|
if self.knockback.0 < FGi32::ZERO {
|
|
return Err("knockback must be non-negative".into());
|
|
}
|
|
if self.knockback_growth < FGi32::ZERO {
|
|
return Err("knockback growth must be non-negative".into());
|
|
}
|
|
if self.end_frame <= self.start_frame {
|
|
return Err("end frame must be greater than start frame".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Knockback calculation
|
|
pub fn calculate_knockback(
|
|
&self,
|
|
receiver_attribs: &FighterAttributes,
|
|
receiver_damage: FighterDamage,
|
|
total_damage_received_this_frame: FighterDamage,
|
|
interrupted_smash_charge: bool,
|
|
receiver_crouching: bool,
|
|
) -> Knockback {
|
|
/// Weight that produces a weight modifier of exactly 1.0. Weight-independent
|
|
/// attacks pass this value in place of the target's real weight.
|
|
const BASELINE_WEIGHT: FGi32 = FGi32::lit("100.0");
|
|
|
|
/// Applied to the weight-scaled damage term.
|
|
const KB_GAIN: FGi32 = FGi32::lit("1.4");
|
|
|
|
/// Constant floor added to the damage term before knockback growth scales it.
|
|
/// At 0% and 0 damage, knockback reduces to KB_OFFSET * s + b.
|
|
const KB_OFFSET: FGi32 = FGi32::lit("18.0");
|
|
|
|
/// Knockback growth is authored in percent (110 means 1.1x).
|
|
const KB_GROWTH_SCALE: FGi32 = FGi32::lit("0.01");
|
|
const DAMAGE_TERM_DIVISOR: FGi32 = FGi32::lit("20.0");
|
|
const DAMAGE_BONUS: FGi32 = FGi32::lit("2.0"); // effective damage floor in the damage term
|
|
const CHARGE_SMASH_INTERRUPTION_MODIFIER: FGi32 = FGi32::lit("1.2");
|
|
const CROUCH_CANCEL_MODIFIER: FGi32 = FGi32::lit("0.666667");
|
|
|
|
let attack_damage_unstalled = self.damage.0;
|
|
|
|
let modifier = {
|
|
let mut out = FGi32::ONE;
|
|
if interrupted_smash_charge {
|
|
out *= CHARGE_SMASH_INTERRUPTION_MODIFIER
|
|
} else if receiver_crouching {
|
|
out *= CROUCH_CANCEL_MODIFIER
|
|
}
|
|
out
|
|
};
|
|
|
|
let kb = match self.knockback_type {
|
|
KnockbackType::Fixed => FGWide::from_num(self.knockback.0),
|
|
KnockbackType::Normal => {
|
|
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)
|
|
}
|
|
};
|
|
|
|
Knockback((FGWide::from_num(modifier) * kb).saturating_to_num())
|
|
}
|
|
}
|
|
|
|
pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<usize>, frame: u32) {
|
|
list.clear();
|
|
list.extend(
|
|
script
|
|
.hitboxes
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, hb)| hb.start_frame <= frame && hb.end_frame > frame)
|
|
.map(|(i, _)| i),
|
|
);
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash)]
|
|
pub struct FighterSolvedHurtbox {
|
|
pub hurtbox_idx: usize,
|
|
pub capsule: FixedAffineCapsule,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash)]
|
|
pub struct FighterSolvedHitbox {
|
|
pub hitbox_idx: usize,
|
|
pub capsule: FixedCapsule,
|
|
}
|
|
|
|
pub fn solve_hitboxes(
|
|
mut query: Query<(
|
|
&FighterBoneMatrices,
|
|
&FighterBoneIndexMap,
|
|
&FighterTranslation,
|
|
&FighterPreviousTranslation,
|
|
&FighterFacingDirection,
|
|
&FighterPreviousFacingDirection,
|
|
&mut FighterHitboxes,
|
|
&FighterAnimationFrame,
|
|
)>,
|
|
attack_scripts: Res<Assets<FighterAttackScript>>,
|
|
) {
|
|
for (
|
|
matrices,
|
|
bone_indices,
|
|
translation,
|
|
previous_translation,
|
|
facing_direction,
|
|
previous_facing_direction,
|
|
mut hitboxes,
|
|
animation_frame,
|
|
) in &mut query
|
|
{
|
|
let Some(active_attack) = hitboxes.0.as_mut() else {
|
|
continue;
|
|
};
|
|
active_attack.solved_hitboxes.clear();
|
|
let attack_script = attack_scripts
|
|
.get(&active_attack.script)
|
|
.expect("Attack script should be valid");
|
|
let fighter_trf = translation.get_3d_transform(facing_direction);
|
|
let previous_fighter_trf = FighterTranslation(previous_translation.0)
|
|
.get_3d_transform(&previous_facing_direction.0);
|
|
for hitbox_idx in active_attack.active_hitboxes.iter().copied() {
|
|
let hitbox = attack_script
|
|
.hitboxes
|
|
.get(hitbox_idx)
|
|
.expect("Hitbox IDX should be valid");
|
|
let matrices = bone_indices
|
|
.0
|
|
.get(&hitbox.bone)
|
|
.and_then(|index| matrices.get_index(*index));
|
|
|
|
if matrices.is_none() {
|
|
warn!("Bone {} not found!", hitbox.bone);
|
|
continue;
|
|
}
|
|
|
|
let (prev_bone_matrix, curr_bone_matrix) = matrices.unwrap();
|
|
|
|
let curr_hbox_matrix = fighter_trf.mul(curr_bone_matrix);
|
|
let curr_hbox_pos = curr_hbox_matrix.transform_point(hitbox.offset);
|
|
|
|
if hitbox.start_frame == animation_frame.frame {
|
|
// Sphere only
|
|
active_attack.solved_hitboxes.push(FighterSolvedHitbox {
|
|
hitbox_idx,
|
|
capsule: FixedCapsule {
|
|
start: curr_hbox_pos,
|
|
end: curr_hbox_pos,
|
|
radius: hitbox.radius,
|
|
},
|
|
})
|
|
} else {
|
|
let prev_hbox_matrix = previous_fighter_trf.mul(prev_bone_matrix);
|
|
let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset);
|
|
active_attack.solved_hitboxes.push(FighterSolvedHitbox {
|
|
hitbox_idx,
|
|
capsule: FixedCapsule {
|
|
start: prev_hbox_pos,
|
|
end: curr_hbox_pos,
|
|
radius: hitbox.radius,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[derive(Component)]
|
|
pub struct FighterAttackScriptAssets {
|
|
pub scripts: HashMap<AttackKind, Handle<FighterAttackScript>>,
|
|
}
|
|
#[derive(Component, Clone, Debug, Default, Hash)]
|
|
pub struct FighterSolvedHurtboxes(pub Vec<FighterSolvedHurtbox>);
|
|
|
|
#[derive(Clone, Debug, Hash)]
|
|
pub struct FighterHit {
|
|
pub attacker: Entity,
|
|
pub defender: Entity,
|
|
pub attack_script: Handle<FighterAttackScript>,
|
|
pub attack_kind: AttackKind,
|
|
pub hitbox_index: usize,
|
|
}
|
|
|
|
#[derive(Resource, Clone, Debug, Default, Hash)]
|
|
pub struct FighterPendingHits(pub Vec<FighterHit>);
|
|
|
|
impl MapEntities for FighterPendingHits {
|
|
fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
|
|
for hit in &mut self.0 {
|
|
hit.attacker = entity_mapper.get_mapped(hit.attacker);
|
|
hit.defender = entity_mapper.get_mapped(hit.defender);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn solve_hurtboxes(
|
|
mut query: Query<(
|
|
&FighterBoneMatrices,
|
|
&FighterBoneIndexMap,
|
|
&FighterTranslation,
|
|
&FighterFacingDirection,
|
|
&mut FighterSolvedHurtboxes,
|
|
&Fighter,
|
|
)>,
|
|
manifests: Res<Assets<FighterManifest>>,
|
|
) {
|
|
for (matrices, bone_indices, translation, facing_direction, mut solved_hurtboxes, fighter) in
|
|
&mut query
|
|
{
|
|
let manifest = manifests
|
|
.get(&fighter.manifest)
|
|
.expect("Manifest should be valid");
|
|
let fighter_trf = translation.get_3d_transform(facing_direction);
|
|
solved_hurtboxes.0.clear();
|
|
for (hurtbox_idx, hurtbox) in manifest.hurtboxes.iter().enumerate() {
|
|
let bone_matrix = bone_indices
|
|
.0
|
|
.get(&hurtbox.bone)
|
|
.and_then(|index| matrices.get_current_index(*index));
|
|
|
|
if let Some(bone_matrix) = bone_matrix {
|
|
let to_global = fighter_trf.mul(bone_matrix);
|
|
let hurtbox_trf = FixedMat4::IDENTITY
|
|
.with_rotation(FGVec3::new(
|
|
hurtbox.rotation.x.to_radians(),
|
|
hurtbox.rotation.y.to_radians(),
|
|
hurtbox.rotation.z.to_radians(),
|
|
))
|
|
.with_translation(hurtbox.offset);
|
|
solved_hurtboxes.0.push(FighterSolvedHurtbox {
|
|
hurtbox_idx,
|
|
capsule: FixedAffineCapsule::new(
|
|
to_global.mul(hurtbox_trf),
|
|
hurtbox.half_length,
|
|
hurtbox.radius,
|
|
),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn intersect_attacks_with_hurtboxes(
|
|
roster: Res<FighterRoster>,
|
|
mut query_attacker: Query<(
|
|
Entity,
|
|
&mut FighterHitboxes,
|
|
&Fighter,
|
|
&Player,
|
|
&FighterStaleMoveQueue,
|
|
)>,
|
|
query_attacked: Query<(Entity, &FighterSolvedHurtboxes)>,
|
|
mut pending_hits: ResMut<FighterPendingHits>,
|
|
attack_scripts: Res<Assets<FighterAttackScript>>,
|
|
) {
|
|
pending_hits.0.clear();
|
|
|
|
for &entity_receiving in &roster.0 {
|
|
let Ok((_, hurtboxes)) = query_attacked.get(entity_receiving) else {
|
|
continue;
|
|
};
|
|
|
|
for &entity_hurting in &roster.0 {
|
|
if entity_hurting == entity_receiving {
|
|
continue;
|
|
}
|
|
let Ok((_, mut hitboxes, _, _, _)) = query_attacker.get_mut(entity_hurting) else {
|
|
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;
|
|
for hurtbox in hurtboxes.0.iter() {
|
|
for solved_hitbox in active_attack.solved_hitboxes.iter() {
|
|
if hurtbox.capsule.intersects(solved_hitbox.capsule) {
|
|
let candidate = solved_hitbox.hitbox_idx;
|
|
if hitting_hitbox_idx.is_none_or(|current: usize| {
|
|
attack_script.hitboxes[candidate].id
|
|
< attack_script.hitboxes[current].id
|
|
}) {
|
|
hitting_hitbox_idx = Some(candidate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let Some(hitting_hitbox_idx) = hitting_hitbox_idx else {
|
|
continue;
|
|
};
|
|
|
|
pending_hits.0.push(FighterHit {
|
|
attacker: entity_hurting,
|
|
defender: entity_receiving,
|
|
attack_script: active_attack.script.clone(),
|
|
attack_kind: active_attack.kind,
|
|
hitbox_index: hitting_hitbox_idx,
|
|
});
|
|
info!("Hit by hitbox {}!!!!", hitting_hitbox_idx);
|
|
|
|
active_attack.hit_fighters.push(entity_receiving);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct FighterAttackPlugin;
|
|
|
|
impl Plugin for FighterAttackPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<FighterPendingHits>()
|
|
.add_plugins(reaction::FighterReactionPlugin)
|
|
.rollback_resource_with_clone::<FighterPendingHits>()
|
|
.update_resource_with_map_entities::<FighterPendingHits>()
|
|
.checksum_resource_with_hash::<FighterPendingHits>();
|
|
app.add_systems(
|
|
GgrsSchedule,
|
|
(
|
|
solve_hurtboxes,
|
|
solve_hitboxes,
|
|
intersect_attacks_with_hurtboxes,
|
|
)
|
|
.chain()
|
|
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
|
|
.in_set(GameplaySet::Attacks),
|
|
);
|
|
}
|
|
}
|