Make combat reaction state rollback-safe (#34)
CI / build-and-test (push) Successful in 13m10s
CI / build-and-test (push) Successful in 13m10s
## Summary - replace the per-fighter received-attack inbox with a typed rollback-registered pending-hit resource - split combat reaction resolution into its own attack module - remap entity references during rollback and include pending hits in debug save states Fixes #21 Reviewed-on: #34 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #34.
This commit is contained in:
@@ -0,0 +1,596 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
mod reaction;
|
||||
|
||||
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)]
|
||||
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(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,
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user