Implement hitstun and knockback.
This commit is contained in:
@@ -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)]
|
||||||
|
|||||||
@@ -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,9 +32,13 @@ 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)>,
|
||||||
animation.advance();
|
) {
|
||||||
|
for (mut animation, hitstop) in &mut fighters {
|
||||||
|
if hitstop.remaining_frames == 0 {
|
||||||
|
animation.advance();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +85,9 @@ pub enum AnimKind {
|
|||||||
Fall,
|
Fall,
|
||||||
Turn,
|
Turn,
|
||||||
|
|
||||||
|
FlinchGround,
|
||||||
|
FlinchAir,
|
||||||
|
|
||||||
// Attacks
|
// Attacks
|
||||||
AttackJab1,
|
AttackJab1,
|
||||||
AttackUpTilt,
|
AttackUpTilt,
|
||||||
|
|||||||
+370
-114
@@ -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,62 +370,57 @@ 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();
|
||||||
..
|
let attack_script = attack_scripts
|
||||||
} = &mut *hitboxes;
|
.get(&active_attack.script)
|
||||||
active_hitboxes_solved.clear();
|
.expect("Attack script should be valid");
|
||||||
if let Some(attack_script) = script_handle {
|
let fighter_trf = translation.get_3d_transform(facing_direction);
|
||||||
let attack_script = attack_scripts
|
let previous_fighter_trf = FighterTranslation(previous_translation.0)
|
||||||
.get(attack_script)
|
.get_3d_transform(&previous_facing_direction.0);
|
||||||
.expect("Attack script should be valid");
|
for hitbox_idx in active_attack.active_hitboxes.iter().copied() {
|
||||||
let fighter_trf = translation.get_3d_transform(facing_direction);
|
let hitbox = attack_script
|
||||||
let previous_fighter_trf = FighterTranslation(previous_translation.0)
|
.hitboxes
|
||||||
.get_3d_transform(&previous_facing_direction.0);
|
.get(hitbox_idx)
|
||||||
for hitbox_idx in active_hitboxes.iter().copied() {
|
.expect("Hitbox IDX should be valid");
|
||||||
let hitbox = attack_script
|
let matrices = bone_indices
|
||||||
.hitboxes
|
.0
|
||||||
.get(hitbox_idx)
|
.get(&hitbox.bone)
|
||||||
.expect("Hitbox IDX should be valid");
|
.and_then(|index| matrices.get_index(*index));
|
||||||
let matrices = bone_indices
|
|
||||||
.0
|
|
||||||
.get(&hitbox.bone)
|
|
||||||
.and_then(|index| matrices.get_index(*index));
|
|
||||||
|
|
||||||
if matrices.is_none() {
|
if matrices.is_none() {
|
||||||
warn!("Bone {} not found!", hitbox.bone);
|
warn!("Bone {} not found!", hitbox.bone);
|
||||||
continue;
|
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_matrix = fighter_trf.mul(curr_bone_matrix);
|
||||||
let curr_hbox_pos = curr_hbox_matrix.transform_point(hitbox.offset);
|
let curr_hbox_pos = curr_hbox_matrix.transform_point(hitbox.offset);
|
||||||
|
|
||||||
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,
|
||||||
end: curr_hbox_pos,
|
end: curr_hbox_pos,
|
||||||
radius: hitbox.radius,
|
radius: hitbox.radius,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} 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,
|
||||||
end: curr_hbox_pos,
|
end: curr_hbox_pos,
|
||||||
radius: hitbox.radius,
|
radius: hitbox.radius,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,63 +491,202 @@ 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 entity_hurting == entity_receiving {
|
||||||
|
continue;
|
||||||
if attack_script.is_none() {
|
}
|
||||||
|
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;
|
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 {
|
let mut hitting_hitbox_idx = None;
|
||||||
if entity_hurting == entity_receiving {
|
for hurtbox in hurtboxes.0.iter() {
|
||||||
continue;
|
for solved_hitbox in active_attack.solved_hitboxes.iter() {
|
||||||
}
|
if hurtbox.capsule.intersects(solved_hitbox.capsule) {
|
||||||
if hitboxes.hit_fighters.contains(&entity_receiving) {
|
let candidate = solved_hitbox.hitbox_idx;
|
||||||
continue;
|
if hitting_hitbox_idx.is_none_or(|current: usize| {
|
||||||
}
|
attack_script.hitboxes[candidate].id
|
||||||
let Ok((_, hurtboxes, _)) = query_attacked.get(entity_receiving) else {
|
< attack_script.hitboxes[current].id
|
||||||
continue;
|
}) {
|
||||||
};
|
hitting_hitbox_idx = Some(candidate);
|
||||||
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 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;
|
pub struct FighterAttackPlugin;
|
||||||
|
|
||||||
impl Plugin for FighterAttackPlugin {
|
impl Plugin for 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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use bevy::reflect::Reflect;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug, Reflect)]
|
||||||
|
pub enum FighterErrors {
|
||||||
|
#[error("Fighter manifest not found")]
|
||||||
|
FighterManifestNotFound,
|
||||||
|
}
|
||||||
+55
-32
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ 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 {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-37
@@ -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)
|
FighterState::Walk(value) => {
|
||||||
.insert((new_state, StateNameDebug(new_state_name)));
|
hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
|
||||||
info!("State {} -> {}", old_state_name, new_state_name);
|
}
|
||||||
}
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user