Implement hitstun and knockback.
This commit is contained in:
+370
-114
@@ -1,37 +1,85 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_ggrs::prelude::GgrsSchedule;
|
||||
use bevy_ggrs::{Rollback, prelude::GgrsSchedule};
|
||||
use fixed_math::SinCos;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
fighter::{
|
||||
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes,
|
||||
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterHitstop,
|
||||
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterRoster,
|
||||
FighterTranslation,
|
||||
animation::{AnimKind, FighterAnimationFrame},
|
||||
baked_animation::{FighterBoneIndexMap, FighterBoneMatrices, FixedMat4},
|
||||
errors::FighterErrors,
|
||||
hurtbox::{FixedAffineCapsule, FixedCapsule},
|
||||
manifest::FighterManifest,
|
||||
},
|
||||
game_settings::GameSettings,
|
||||
math::{
|
||||
int::{FGi32, FGi32Ext},
|
||||
int::{FGWide, FGi32, FGi32Ext},
|
||||
vec::FGVec2,
|
||||
vec3::FGVec3,
|
||||
},
|
||||
player::Player,
|
||||
schedule::GameplaySet,
|
||||
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);
|
||||
|
||||
#[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 {
|
||||
pub fn lit(text: &str) -> Self {
|
||||
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 {
|
||||
Normal,
|
||||
Fixed,
|
||||
@@ -54,6 +102,22 @@ 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)]
|
||||
@@ -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 {
|
||||
#[default]
|
||||
NeutralAir,
|
||||
ForwardAir,
|
||||
BackAir,
|
||||
@@ -129,7 +194,51 @@ pub struct AttackHitbox {
|
||||
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 {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
@@ -157,8 +266,6 @@ impl AttackHitbox {
|
||||
/// Knockback calculation
|
||||
pub fn calculate_knockback(
|
||||
&self,
|
||||
_attack: AttackKind,
|
||||
|
||||
receiver_attribs: &FighterAttributes,
|
||||
receiver_damage: FighterDamage,
|
||||
total_damage_received_this_frame: FighterDamage,
|
||||
@@ -195,16 +302,23 @@ impl AttackHitbox {
|
||||
out
|
||||
};
|
||||
|
||||
let s = KB_GROWTH_SCALE * self.knockback_growth;
|
||||
let p = receiver_damage.0.floor() + total_damage_received_this_frame.0;
|
||||
let d = attack_damage_unstalled;
|
||||
let damage_term = p / DAMAGE_TERM_DIVISOR * (d + DAMAGE_BONUS);
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
let scaled = damage_term * (FGi32::lit("2.0") * BASELINE_WEIGHT)
|
||||
/ (receiver_attribs.weight + BASELINE_WEIGHT);
|
||||
let kb = (scaled * KB_GAIN + KB_OFFSET) * s + self.knockback.0;
|
||||
|
||||
Knockback(modifier * kb)
|
||||
Knockback((FGWide::from_num(modifier) * kb).saturating_to_num())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +346,6 @@ pub struct FighterSolvedHitbox {
|
||||
pub capsule: FixedCapsule,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default, Hash)]
|
||||
pub struct FighterSolvedHitboxes(pub Vec<FighterSolvedHitbox>);
|
||||
|
||||
pub fn solve_hitboxes(
|
||||
mut query: Query<(
|
||||
&FighterBoneMatrices,
|
||||
@@ -259,62 +370,57 @@ pub fn solve_hitboxes(
|
||||
animation_frame,
|
||||
) in &mut query
|
||||
{
|
||||
let FighterHitboxes {
|
||||
attack_script: script_handle,
|
||||
active_hitboxes,
|
||||
active_hitboxes_solved,
|
||||
..
|
||||
} = &mut *hitboxes;
|
||||
active_hitboxes_solved.clear();
|
||||
if let Some(attack_script) = script_handle {
|
||||
let attack_script = attack_scripts
|
||||
.get(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_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));
|
||||
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;
|
||||
}
|
||||
if matrices.is_none() {
|
||||
warn!("Bone {} not found!", hitbox.bone);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (prev_bone_matrix, curr_bone_matrix) = matrices.unwrap();
|
||||
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);
|
||||
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_hitboxes_solved.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_hitboxes_solved.push(FighterSolvedHitbox {
|
||||
hitbox_idx,
|
||||
capsule: FixedCapsule {
|
||||
start: prev_hbox_pos,
|
||||
end: curr_hbox_pos,
|
||||
radius: hitbox.radius,
|
||||
},
|
||||
});
|
||||
}
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,6 +432,16 @@ pub struct FighterAttackScriptAssets {
|
||||
#[derive(Component, Clone, Debug, Default, Hash)]
|
||||
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(
|
||||
mut query: Query<(
|
||||
&FighterBoneMatrices,
|
||||
@@ -375,63 +491,202 @@ pub fn solve_hurtboxes(
|
||||
|
||||
pub fn intersect_attacks_with_hurtboxes(
|
||||
roster: Res<FighterRoster>,
|
||||
mut query_attacker: Query<(Entity, &mut FighterHitboxes, &Fighter, &Player)>,
|
||||
query_attacked: Query<(Entity, &FighterSolvedHurtboxes, &Player)>,
|
||||
mut query_attacker: Query<(
|
||||
Entity,
|
||||
&mut FighterHitboxes,
|
||||
&Fighter,
|
||||
&Player,
|
||||
&FighterStaleMoveQueue,
|
||||
)>,
|
||||
mut query_attacked: Query<(
|
||||
Entity,
|
||||
&FighterSolvedHurtboxes,
|
||||
&Player,
|
||||
&Fighter,
|
||||
&mut FighterReceivedAttacks,
|
||||
)>,
|
||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||
) {
|
||||
for &entity_hurting in &roster.0 {
|
||||
let Ok((_, mut hitboxes, _, _)) = query_attacker.get_mut(entity_hurting) else {
|
||||
for &entity_receiving in &roster.0 {
|
||||
let Ok((_, hurtboxes, _, _receiving_fighter, mut received_attacks)) =
|
||||
query_attacked.get_mut(entity_receiving)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if hitboxes.attack_script.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ref attack_script) = hitboxes.attack_script {
|
||||
let attack_script = attack_scripts.get(attack_script);
|
||||
|
||||
if attack_script.is_none() {
|
||||
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_script.unwrap();
|
||||
let attack_script = attack_scripts
|
||||
.get(&active_attack.script)
|
||||
.expect("Attack script should be valid");
|
||||
|
||||
for &entity_receiving in &roster.0 {
|
||||
if entity_hurting == entity_receiving {
|
||||
continue;
|
||||
}
|
||||
if hitboxes.hit_fighters.contains(&entity_receiving) {
|
||||
continue;
|
||||
}
|
||||
let Ok((_, hurtboxes, _)) = query_attacked.get(entity_receiving) else {
|
||||
continue;
|
||||
};
|
||||
let mut hitting_hitbox_idx = None;
|
||||
for hurtbox in hurtboxes.0.iter() {
|
||||
for solved_hitbox in hitboxes.active_hitboxes_solved.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 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;
|
||||
};
|
||||
|
||||
hitboxes.hit_fighters.push(entity_receiving);
|
||||
info!("Hit by hitbox {}!!!!", hitting_hitbox_idx);
|
||||
}
|
||||
|
||||
let Some(hitting_hitbox_idx) = hitting_hitbox_idx else {
|
||||
continue;
|
||||
};
|
||||
|
||||
received_attacks.push((
|
||||
entity_hurting,
|
||||
active_attack.script.clone(),
|
||||
active_attack.kind,
|
||||
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;
|
||||
|
||||
impl Plugin for FighterAttackPlugin {
|
||||
@@ -442,10 +697,11 @@ impl Plugin for FighterAttackPlugin {
|
||||
solve_hurtboxes,
|
||||
solve_hitboxes,
|
||||
intersect_attacks_with_hurtboxes,
|
||||
calculate_received_attacks_trajectories,
|
||||
)
|
||||
.chain()
|
||||
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
|
||||
.in_set(GameplaySet::Animation),
|
||||
.in_set(GameplaySet::Attacks),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user