Attack system revamp (#82)
- Add crouch cancelling - Add IASA for aerial attacks - Add scaffolding for autocancel - Add hitbox group system - Add new ergonomic features for attack scripting - Add more information to the hitbox viewer - Improve the looks of debug capsules Reviewed-on: #82 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #82.
This commit is contained in:
@@ -33,7 +33,7 @@ use crate::{
|
||||
#[derive(Deref, Component, Default, Hash, Debug, Copy, Clone)]
|
||||
pub struct FighterKnockbackVelocity(pub FGVec2);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash, PartialEq)]
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FighterDamage(FGi32);
|
||||
|
||||
impl std::fmt::Display for FighterDamage {
|
||||
@@ -52,7 +52,8 @@ pub enum HitReactionKind {
|
||||
pub struct FighterHitReaction {
|
||||
pub launch_velocity: FGi32,
|
||||
pub launch_angle: FGi32,
|
||||
pub hitstop_frames: HitStop,
|
||||
pub victim_hitstop_frames: HitStop,
|
||||
pub attacker_hitstop_frames: HitStop,
|
||||
pub hitstun_frames: u32,
|
||||
pub kind: HitReactionKind,
|
||||
pub damage: FighterDamage,
|
||||
@@ -80,7 +81,7 @@ impl HitStop {
|
||||
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 a = (attack_damage_stalled.0 / FGi32::lit("3.0") + FGi32::lit("3.0")).round_to_zero();
|
||||
let crouch_cancelling_factor = if crouch_cancelling {
|
||||
FGi32::lit("0.66666666")
|
||||
} else {
|
||||
@@ -97,6 +98,12 @@ impl HitStop {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HitStop {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum KnockbackType {
|
||||
Normal,
|
||||
@@ -113,9 +120,15 @@ impl KnockbackType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Knockback(FGi32);
|
||||
|
||||
impl std::fmt::Display for Knockback {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:.2}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Knockback {
|
||||
pub fn lit(val: &str) -> Self {
|
||||
Self(FGi32::lit(val))
|
||||
@@ -144,6 +157,15 @@ pub enum AttackAngle {
|
||||
Sakurai,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AttackAngle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AttackAngle::Normal(fixed_i32) => write!(f, "{:.2}º", fixed_i32),
|
||||
AttackAngle::Sakurai => write!(f, "Sakurai"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AttackAngle {
|
||||
fn default() -> Self {
|
||||
AttackAngle::Normal(FGi32::lit("0"))
|
||||
@@ -175,6 +197,13 @@ impl AttackAngle {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_melee_angle_representation(&self) -> FGi32 {
|
||||
match self {
|
||||
AttackAngle::Normal(fixed_i32) => *fixed_i32,
|
||||
AttackAngle::Sakurai => FGi32::lit("361"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
|
||||
@@ -217,6 +246,7 @@ impl AttackKind {
|
||||
#[reflect(opaque)]
|
||||
pub struct AttackHitbox {
|
||||
pub id: u32,
|
||||
pub group: u32,
|
||||
pub bone: String,
|
||||
pub damage: FighterDamage,
|
||||
pub offset: FGVec3,
|
||||
@@ -229,6 +259,14 @@ pub struct AttackHitbox {
|
||||
pub end_frame: u32,
|
||||
}
|
||||
|
||||
static ATTACK_HITBOX_ID_PALETTE: [bevy::color::Srgba; 5] = [
|
||||
bevy::color::palettes::tailwind::RED_500,
|
||||
bevy::color::palettes::tailwind::GREEN_500,
|
||||
bevy::color::palettes::tailwind::BLUE_500,
|
||||
bevy::color::palettes::tailwind::FUCHSIA_500,
|
||||
bevy::color::palettes::tailwind::ORANGE_500,
|
||||
];
|
||||
|
||||
const STALE_MOVE_QUEUE_COUNT: usize = 9;
|
||||
|
||||
#[derive(Component, Hash, Clone, Default)]
|
||||
@@ -276,6 +314,10 @@ impl FighterStaleMoveQueue {
|
||||
}
|
||||
|
||||
impl AttackHitbox {
|
||||
pub fn get_debug_draw_color(&self) -> bevy::color::Srgba {
|
||||
ATTACK_HITBOX_ID_PALETTE[self.id as usize % ATTACK_HITBOX_ID_PALETTE.len()]
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.bone.trim().is_empty() {
|
||||
return Err("bone name is empty".into());
|
||||
@@ -369,6 +411,36 @@ pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<us
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates both the currently active hitbox intervals and the lifetime of
|
||||
/// their per-group victim histories. A group remains immune only while at
|
||||
/// least one hitbox belonging to that group is active.
|
||||
pub fn update_active_attack(
|
||||
script: &FighterAttackScript,
|
||||
attack: &mut super::FighterActiveAttack,
|
||||
frame: u32,
|
||||
) {
|
||||
update_active_hitbox_list(script, &mut attack.active_hitboxes, frame);
|
||||
|
||||
let mut active_groups = Vec::new();
|
||||
for &index in &attack.active_hitboxes {
|
||||
let group = script.hitboxes[index].group;
|
||||
if !active_groups.contains(&group) {
|
||||
active_groups.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
attack.hit_groups.retain(|state| active_groups.contains(&state.group));
|
||||
for group in active_groups {
|
||||
if !attack.hit_groups.iter().any(|state| state.group == group) {
|
||||
attack.hit_groups.push(super::FighterHitGroupState {
|
||||
group,
|
||||
hit_fighters: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
attack.hit_groups.sort_unstable_by_key(|state| state.group);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||
pub struct FighterSolvedHurtbox {
|
||||
pub hurtbox_idx: usize,
|
||||
@@ -571,10 +643,6 @@ pub fn intersect_attacks_with_hurtboxes(
|
||||
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");
|
||||
@@ -584,6 +652,15 @@ pub fn intersect_attacks_with_hurtboxes(
|
||||
for solved_hitbox in active_attack.solved_hitboxes.iter() {
|
||||
if hurtbox.capsule.intersects(solved_hitbox.capsule) {
|
||||
let candidate = solved_hitbox.hitbox_idx;
|
||||
let group = attack_script.hitboxes[candidate].group;
|
||||
let already_hit = active_attack
|
||||
.hit_groups
|
||||
.iter()
|
||||
.find(|state| state.group == group)
|
||||
.is_some_and(|state| state.hit_fighters.contains(&entity_receiving));
|
||||
if already_hit {
|
||||
continue;
|
||||
}
|
||||
if hitting_hitbox_idx.is_none_or(|current: usize| {
|
||||
attack_script.hitboxes[candidate].id
|
||||
< attack_script.hitboxes[current].id
|
||||
@@ -606,7 +683,14 @@ pub fn intersect_attacks_with_hurtboxes(
|
||||
hitbox_index: hitting_hitbox_idx,
|
||||
});
|
||||
|
||||
active_attack.hit_fighters.push(entity_receiving);
|
||||
let group = attack_script.hitboxes[hitting_hitbox_idx].group;
|
||||
if let Some(state) = active_attack
|
||||
.hit_groups
|
||||
.iter_mut()
|
||||
.find(|state| state.group == group)
|
||||
{
|
||||
state.hit_fighters.push(entity_receiving);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user