Implement Melee-style knockback stacking (#97)
## Summary - stack residual airborne knockback per axis after the configured launch-frame threshold - apply DI after combining the launch vectors while retaining the second hit's hitstun - preserve and decay residual knockback after hitstun - make stacking state rollback-safe and include it in debug save states - clear launch state on landing, ledge interactions, KO, and respawn ## Testing - `cargo check --tests` - `cargo test` (159 tests passed) - `git diff --check` ## Asset configuration For Melee timing, `knockback_stacking_window` should be set to `10`. The local ignored asset package has been updated accordingly. Reviewed-on: #97 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #97.
This commit is contained in:
@@ -9,8 +9,9 @@ use crate::{
|
||||
FighterTranslation, FighterVelocity,
|
||||
animation::FighterAnimationFrame,
|
||||
attack::{
|
||||
FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits,
|
||||
FighterSolvedHurtboxes, FighterStaleMoveQueue,
|
||||
FighterDamage, FighterKnockbackState, FighterKnockbackVelocity,
|
||||
FighterPendingHitReaction, FighterPendingHits, FighterSolvedHurtboxes,
|
||||
FighterStaleMoveQueue,
|
||||
},
|
||||
baked_animation::FighterBoneMatrices,
|
||||
ecb::FighterDesiredECB,
|
||||
@@ -55,6 +56,7 @@ struct FighterCheckpoint {
|
||||
hitstop: FighterHitstop,
|
||||
hitstop_shake: FighterHitstopShake,
|
||||
knockback_velocity: FighterKnockbackVelocity,
|
||||
knockback_state: FighterKnockbackState,
|
||||
stale_moves: FighterStaleMoveQueue,
|
||||
hitboxes: FighterHitboxes,
|
||||
solved_hurtboxes: FighterSolvedHurtboxes,
|
||||
@@ -160,6 +162,7 @@ pub(super) fn save_or_restore(world: &mut World) {
|
||||
entity.insert(saved.hitstop);
|
||||
entity.insert(saved.hitstop_shake);
|
||||
entity.insert(saved.knockback_velocity);
|
||||
entity.insert(saved.knockback_state);
|
||||
entity.insert(saved.stale_moves.clone());
|
||||
entity.insert(saved.hitboxes.clone());
|
||||
entity.insert(saved.solved_hurtboxes.clone());
|
||||
@@ -199,6 +202,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option<FighterCheckpoint> {
|
||||
hitstop: *fighter.get::<FighterHitstop>()?,
|
||||
hitstop_shake: *fighter.get::<FighterHitstopShake>()?,
|
||||
knockback_velocity: *fighter.get::<FighterKnockbackVelocity>()?,
|
||||
knockback_state: *fighter.get::<FighterKnockbackState>()?,
|
||||
stale_moves: fighter.get::<FighterStaleMoveQueue>()?.clone(),
|
||||
hitboxes: fighter.get::<FighterHitboxes>()?.clone(),
|
||||
solved_hurtboxes: fighter.get::<FighterSolvedHurtboxes>()?.clone(),
|
||||
|
||||
@@ -15,6 +15,16 @@ pub fn apply_directional_influence(
|
||||
let (sin, cos) = attack_angle.sin_cos();
|
||||
let base_launch_direction = FGVec2::new(cos, sin);
|
||||
|
||||
apply_directional_influence_to_direction(receiver_stick, base_launch_direction, di_max_angle)
|
||||
}
|
||||
|
||||
/// Applies DI to an already-computed launch direction. This is used after
|
||||
/// knockback stacking, where the resulting angle may differ from either hit.
|
||||
pub fn apply_directional_influence_to_direction(
|
||||
receiver_stick: FGVec2,
|
||||
base_launch_direction: FGVec2,
|
||||
di_max_angle: FGi32,
|
||||
) -> FGVec2 {
|
||||
let Some(base_launch_direction) = base_launch_direction.normalize() else {
|
||||
return FGVec2::ZERO;
|
||||
};
|
||||
@@ -30,14 +40,20 @@ pub fn apply_directional_influence(
|
||||
-di_max_angle.to_fixed::<FGWide>(),
|
||||
di_max_angle.to_fixed::<FGWide>(),
|
||||
);
|
||||
let (sin, cos) = (attack_angle + angle_change.to_fixed::<FGi32>()).sin_cos();
|
||||
FGVec2::new(cos, sin).normalize_or_zero()
|
||||
let (sin, cos) = angle_change.to_fixed::<FGi32>().sin_cos();
|
||||
FGVec2::new(
|
||||
base_launch_direction.x * cos - base_launch_direction.y * sin,
|
||||
base_launch_direction.x * sin + base_launch_direction.y * cos,
|
||||
)
|
||||
.normalize_or_zero()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
fighter::attack::directional_influence::apply_directional_influence,
|
||||
fighter::attack::directional_influence::{
|
||||
apply_directional_influence, apply_directional_influence_to_direction,
|
||||
},
|
||||
math::{int::FGi32, vec::FGVec2},
|
||||
};
|
||||
|
||||
@@ -66,4 +82,17 @@ mod test {
|
||||
assert!(di.x > FGi32::ZERO);
|
||||
assert!(di.y < FGi32::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_di_preserves_a_stacked_direction_without_input() {
|
||||
let direction = FGVec2::lit("-0.6", "0.8");
|
||||
let result = apply_directional_influence_to_direction(
|
||||
FGVec2::ZERO,
|
||||
direction,
|
||||
FGi32::lit("18.0"),
|
||||
);
|
||||
const EPSILON: FGi32 = FGi32::from_bits(6);
|
||||
assert!(result.x.abs_diff(direction.x) < EPSILON);
|
||||
assert!(result.y.abs_diff(direction.y) < EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,75 @@ use crate::{
|
||||
#[derive(Deref, Component, Default, Hash, Debug, Copy, Clone)]
|
||||
pub struct FighterKnockbackVelocity(pub FGVec2);
|
||||
|
||||
/// Lifecycle state for the currently active launch velocity.
|
||||
///
|
||||
/// This counts frames where knockback was actually applied to movement; hitstop
|
||||
/// therefore does not advance it. Melee uses this age to decide whether a
|
||||
/// subsequent airborne hit replaces or stacks with the previous launch.
|
||||
#[derive(Component, Default, Hash, Debug, Copy, Clone)]
|
||||
pub struct FighterKnockbackState {
|
||||
pub frames_since_launch: u32,
|
||||
}
|
||||
|
||||
impl FighterKnockbackState {
|
||||
pub fn restart(&mut self) {
|
||||
self.frames_since_launch = 0;
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.frames_since_launch = 0;
|
||||
}
|
||||
|
||||
pub fn advance(&mut self) {
|
||||
self.frames_since_launch = self.frames_since_launch.saturating_add(1);
|
||||
}
|
||||
|
||||
pub fn can_stack(&self, minimum_frames: u32) -> bool {
|
||||
self.frames_since_launch >= minimum_frames
|
||||
}
|
||||
}
|
||||
|
||||
fn stack_knockback_component(previous: FGi32, new: FGi32) -> FGi32 {
|
||||
if previous.is_zero() {
|
||||
return new;
|
||||
}
|
||||
if new.is_zero() {
|
||||
return previous;
|
||||
}
|
||||
|
||||
if previous.signum() != new.signum() {
|
||||
previous + new
|
||||
} else if previous.abs() > new.abs() {
|
||||
previous
|
||||
} else {
|
||||
new
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies Melee's knockback-stacking rule independently to both axes.
|
||||
pub fn stack_knockback(previous: FGVec2, new: FGVec2) -> FGVec2 {
|
||||
FGVec2::new(
|
||||
stack_knockback_component(previous.x, new.x),
|
||||
stack_knockback_component(previous.y, new.y),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn decay_knockback(
|
||||
velocity: &mut FighterKnockbackVelocity,
|
||||
state: &mut FighterKnockbackState,
|
||||
decay_per_frame: FGi32,
|
||||
) {
|
||||
let speed = velocity.length();
|
||||
let next_speed = speed.saturating_sub(decay_per_frame).max(FGi32::ZERO);
|
||||
velocity.0 = velocity.normalize_or_zero() * next_speed;
|
||||
|
||||
if next_speed.is_zero() {
|
||||
state.clear();
|
||||
} else {
|
||||
state.advance();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FighterDamage(FGi32);
|
||||
|
||||
@@ -73,6 +142,53 @@ pub struct FighterLastAttackDebug {
|
||||
pub trajectory_points: Vec<Vec3>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod knockback_stacking_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn same_direction_keeps_the_stronger_component() {
|
||||
assert_eq!(
|
||||
stack_knockback(FGVec2::lit("3", "8"), FGVec2::lit("5", "4")),
|
||||
FGVec2::lit("5", "8")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opposing_components_add_independently() {
|
||||
assert_eq!(
|
||||
stack_knockback(FGVec2::lit("3", "8"), FGVec2::lit("-5", "-3")),
|
||||
FGVec2::lit("-2", "5")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opposing_components_can_cancel() {
|
||||
assert_eq!(
|
||||
stack_knockback(FGVec2::lit("3", "-4"), FGVec2::lit("-3", "4")),
|
||||
FGVec2::ZERO
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_components_preserve_the_nonzero_value() {
|
||||
assert_eq!(
|
||||
stack_knockback(FGVec2::lit("0", "2"), FGVec2::lit("-3", "0")),
|
||||
FGVec2::lit("-3", "2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacking_threshold_is_inclusive() {
|
||||
let mut state = FighterKnockbackState {
|
||||
frames_since_launch: 9,
|
||||
};
|
||||
assert!(!state.can_stack(10));
|
||||
state.advance();
|
||||
assert!(state.can_stack(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deref, DerefMut, Hash, PartialEq, PartialOrd)]
|
||||
pub struct HitStop(u32);
|
||||
|
||||
|
||||
+5
-2
@@ -41,8 +41,8 @@ use crate::{
|
||||
fighter::{
|
||||
attack::{
|
||||
AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage,
|
||||
FighterKnockbackVelocity, FighterPendingHitReaction, FighterSolvedHurtboxes,
|
||||
FighterStaleMoveQueue,
|
||||
FighterKnockbackState, FighterKnockbackVelocity, FighterPendingHitReaction,
|
||||
FighterSolvedHurtboxes, FighterStaleMoveQueue,
|
||||
},
|
||||
baked_animation::FighterBoneIndexMap,
|
||||
ecb::{FighterDesiredECB, FighterECBBoneIndices},
|
||||
@@ -212,6 +212,7 @@ pub struct FighterPendingRespawn(bool);
|
||||
FighterHitstop,
|
||||
FighterLedgeInformation,
|
||||
FighterKnockbackVelocity,
|
||||
FighterKnockbackState,
|
||||
FighterTranslation,
|
||||
FighterInvulnerabilityState,
|
||||
FighterInvulnerabilityVisualState
|
||||
@@ -330,6 +331,8 @@ impl Plugin for FighterPlugin {
|
||||
.checksum_component_with_hash::<FighterInvulnerabilityState>()
|
||||
.rollback_component_with_copy::<FighterKnockbackVelocity>()
|
||||
.checksum_component_with_hash::<FighterKnockbackVelocity>()
|
||||
.rollback_component_with_copy::<FighterKnockbackState>()
|
||||
.checksum_component_with_hash::<FighterKnockbackState>()
|
||||
.rollback_component_with_clone::<baked_animation::FighterBoneMatrices>()
|
||||
.checksum_component_with_hash::<baked_animation::FighterBoneMatrices>()
|
||||
// Debug views.
|
||||
|
||||
@@ -93,6 +93,19 @@ pub fn apply_air_motion(
|
||||
translation.0 += velocity.0;
|
||||
}
|
||||
|
||||
/// Applies ordinary and launch velocity together, then advances launch decay.
|
||||
/// Hitstop skips state updates entirely, so neither decay nor launch age moves
|
||||
/// while the fighter is frozen.
|
||||
pub fn apply_air_motion_with_knockback(ctx: &mut crate::fighter::state::UpdateContext) {
|
||||
let combined_velocity = FighterVelocity(ctx.velocity.0 + ctx.knockback_velocity.0);
|
||||
apply_air_motion(&combined_velocity, ctx.translation, ctx.prev_translation);
|
||||
attack::decay_knockback(
|
||||
ctx.knockback_velocity,
|
||||
ctx.knockback_state,
|
||||
ctx.game_settings.fighter_common.knockback_decay_per_frame,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_standard_motion(
|
||||
ctx: &mut crate::fighter::state::UpdateContext,
|
||||
airborne_common: &mut AirborneStateCommon,
|
||||
@@ -109,7 +122,7 @@ pub fn update_standard_motion(
|
||||
airborne_common.is_fast_falling,
|
||||
);
|
||||
apply_air_drift(ctx.velocity, ctx.fighter_attributes, ctx.input);
|
||||
apply_air_motion(ctx.velocity, ctx.translation, ctx.prev_translation);
|
||||
apply_air_motion_with_knockback(ctx);
|
||||
}
|
||||
|
||||
pub fn air_interrupts_common(
|
||||
@@ -141,6 +154,8 @@ pub fn collision_interrupt(
|
||||
*ctx.desired_ecb = crate::fighter::ecb::FighterDesiredECB::from(*ctx.ecb);
|
||||
ctx.presentation_snap.0 = true;
|
||||
ctx.surface.land(res.line_id, ctx.ledge_info);
|
||||
ctx.knockback_velocity.0 = FGVec2::ZERO;
|
||||
ctx.knockback_state.clear();
|
||||
Some(FighterState::Land(LandingState::create(landing_type)))
|
||||
}
|
||||
|
||||
|
||||
@@ -33,11 +33,7 @@ impl FighterStateImpl for AirDodgeState {
|
||||
}
|
||||
state_context.velocity.0 *= state_context.fighter_attributes.air_dodge_decay;
|
||||
|
||||
air::apply_air_motion(
|
||||
state_context.velocity,
|
||||
state_context.translation,
|
||||
state_context.prev_translation,
|
||||
);
|
||||
air::apply_air_motion_with_knockback(state_context);
|
||||
}
|
||||
|
||||
fn check_collision_interrupt(
|
||||
@@ -59,6 +55,8 @@ impl FighterStateImpl for AirDodgeState {
|
||||
state_context.ecb.bottom = point_in_segment - state_context.translation.0;
|
||||
state_context.ecb.rebase_onto_bottom();
|
||||
state_context.surface.land(StageLineID { polygon: poly_idx, segment: segment_idx }, state_context.ledge_info);
|
||||
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_state.clear();
|
||||
return Some(FighterState::Land(LandingState::create(LandingType::Special)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +260,7 @@ pub fn collision_interrupt(ctx: &mut CollisionContext, can_walk_off: bool) -> Op
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fighter::attack::{FighterKnockbackState, FighterKnockbackVelocity};
|
||||
use crate::fighter::ecb::{FighterDesiredECB, FighterPreviousECB};
|
||||
use crate::fighter::{
|
||||
FighterECB, FighterLedgeInformation, FighterPreviousTranslation, FighterSurfaceState,
|
||||
@@ -388,6 +389,8 @@ mod tests {
|
||||
let mut translation = FighterTranslation(FGVec2::lit("11", "0"));
|
||||
let previous_translation = FighterPreviousTranslation(translation.0);
|
||||
let mut velocity = FighterVelocity::default();
|
||||
let mut knockback_velocity = FighterKnockbackVelocity::default();
|
||||
let mut knockback_state = FighterKnockbackState::default();
|
||||
let mut ledge_info = FighterLedgeInformation::default();
|
||||
let mut ecb = FighterECB {
|
||||
bottom: FGVec2::lit("0.0", "-0.5"),
|
||||
@@ -403,6 +406,8 @@ mod tests {
|
||||
prev_translation: &previous_translation,
|
||||
input: &FighterInput::default(),
|
||||
velocity: &mut velocity,
|
||||
knockback_velocity: &mut knockback_velocity,
|
||||
knockback_state: &mut knockback_state,
|
||||
surface: &mut surface,
|
||||
ecb: &mut ecb,
|
||||
desired_ecb: &mut desired_ecb,
|
||||
@@ -453,6 +458,8 @@ mod tests {
|
||||
let mut translation = FighterTranslation(FGVec2::lit("11", "0"));
|
||||
let previous_translation = FighterPreviousTranslation(translation.0);
|
||||
let mut velocity = FighterVelocity::default();
|
||||
let mut knockback_velocity = FighterKnockbackVelocity::default();
|
||||
let mut knockback_state = FighterKnockbackState::default();
|
||||
let mut ecb = FighterECB {
|
||||
bottom: FGVec2::lit("0.0", "-0.5"),
|
||||
top: FGVec2::lit("0.0", "0.5"),
|
||||
@@ -468,6 +475,8 @@ mod tests {
|
||||
prev_translation: &previous_translation,
|
||||
input: &FighterInput::default(),
|
||||
velocity: &mut velocity,
|
||||
knockback_velocity: &mut knockback_velocity,
|
||||
knockback_state: &mut knockback_state,
|
||||
surface: &mut surface,
|
||||
ecb: &mut ecb,
|
||||
desired_ecb: &mut desired_ecb,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
fighter::{
|
||||
FighterVelocity, animation::AnimKind, attack::{self, FighterHitReaction}, state::{
|
||||
animation::AnimKind, attack::{self, FighterHitReaction}, state::{
|
||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl,
|
||||
InterruptContext, UpdateContext,
|
||||
air::{self, AirborneStateCommon},
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
land::LandingType,
|
||||
wait::WaitState,
|
||||
},
|
||||
}, math::{int::FGi32, vec::FGVec2},
|
||||
}, math::vec::FGVec2,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
@@ -23,14 +23,20 @@ pub struct HitstunState {
|
||||
pub hit_reaction: FighterHitReaction,
|
||||
pub hitstop_kind: HitstopStateKind,
|
||||
knockback_applied: bool,
|
||||
stack_previous_knockback: bool,
|
||||
}
|
||||
|
||||
impl HitstunState {
|
||||
pub fn new(hit_reaction: FighterHitReaction, hitstop_kind: HitstopStateKind) -> Self {
|
||||
pub fn new(
|
||||
hit_reaction: FighterHitReaction,
|
||||
hitstop_kind: HitstopStateKind,
|
||||
stack_previous_knockback: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
hit_reaction,
|
||||
hitstop_kind,
|
||||
knockback_applied: false,
|
||||
stack_previous_knockback,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +66,12 @@ impl FighterStateImpl for HitstunState {
|
||||
state_context.velocity.0 = FGVec2::ZERO;
|
||||
}
|
||||
|
||||
fn on_exit(&mut self, _state_context: &mut ExitContext) {}
|
||||
fn on_exit(&mut self, state_context: &mut ExitContext) {
|
||||
if matches!(self.hitstop_kind, HitstopStateKind::Grounded) {
|
||||
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_state.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
let hitstop_kind = match self.hitstop_kind {
|
||||
@@ -78,13 +89,27 @@ impl FighterStateImpl for HitstunState {
|
||||
|
||||
fn update(&mut self, ctx: &mut UpdateContext) {
|
||||
if !self.knockback_applied {
|
||||
let launch_direction = attack::directional_influence::apply_directional_influence(
|
||||
ctx.input.get_last_frame().movement,
|
||||
let new_launch_direction = attack::directional_influence::apply_directional_influence(
|
||||
FGVec2::ZERO,
|
||||
self.hit_reaction.launch_angle,
|
||||
ctx.game_settings.fighter_common.di_max_angle,
|
||||
);
|
||||
let new_knockback = new_launch_direction * self.hit_reaction.launch_velocity;
|
||||
let combined_knockback = if self.stack_previous_knockback {
|
||||
attack::stack_knockback(ctx.knockback_velocity.0, new_knockback)
|
||||
} else {
|
||||
new_knockback
|
||||
};
|
||||
let launch_speed = combined_knockback.length();
|
||||
let launch_direction =
|
||||
attack::directional_influence::apply_directional_influence_to_direction(
|
||||
ctx.input.get_last_frame().movement,
|
||||
combined_knockback.normalize_or_zero(),
|
||||
ctx.game_settings.fighter_common.di_max_angle,
|
||||
);
|
||||
|
||||
ctx.knockback_velocity.0 = launch_direction * self.hit_reaction.launch_velocity;
|
||||
ctx.knockback_velocity.0 = launch_direction * launch_speed;
|
||||
ctx.knockback_state.restart();
|
||||
self.knockback_applied = true;
|
||||
}
|
||||
|
||||
@@ -92,17 +117,17 @@ impl FighterStateImpl for HitstunState {
|
||||
HitstopStateKind::Grounded => {}
|
||||
HitstopStateKind::Airborne => {
|
||||
air::integrate_gravity(ctx.velocity, ctx.fighter_attributes, false);
|
||||
let vel_with_kb = FighterVelocity(ctx.velocity.0 + ctx.knockback_velocity.0);
|
||||
air::apply_air_motion(&vel_with_kb, ctx.translation, ctx.prev_translation);
|
||||
air::apply_air_motion_with_knockback(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if matches!(self.hitstop_kind, HitstopStateKind::Grounded) {
|
||||
attack::decay_knockback(
|
||||
ctx.knockback_velocity,
|
||||
ctx.knockback_state,
|
||||
ctx.game_settings.fighter_common.knockback_decay_per_frame,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_collision_interrupt(
|
||||
|
||||
@@ -84,6 +84,7 @@ impl FighterStateImpl for LedgeActionState {
|
||||
|
||||
state_context.velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_state.clear();
|
||||
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -36,6 +36,8 @@ impl FighterStateImpl for LedgeGrabState {
|
||||
|
||||
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||
state_context.play_animation(AnimKind::LedgeGrab, false);
|
||||
state_context.knockback_velocity.0 = crate::math::vec::FGVec2::ZERO;
|
||||
state_context.knockback_state.clear();
|
||||
|
||||
*state_context.vulnerability = FighterInvulnerabilityState::Intangible {
|
||||
frames_remaining: None,
|
||||
|
||||
@@ -65,6 +65,7 @@ impl FighterStateImpl for LedgeWaitState {
|
||||
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||
state_context.velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||
state_context.knockback_state.clear();
|
||||
|
||||
if state_context.ledge_info.grab_count < state_context.game_settings.fighter_common.max_ledge_grab_count {
|
||||
*state_context.vulnerability = FighterInvulnerabilityState::Intangible {
|
||||
|
||||
@@ -7,8 +7,8 @@ use bevy::prelude::*;
|
||||
|
||||
use crate::fighter::animation::{AnimKind, FighterAnimationFrame};
|
||||
use crate::fighter::attack::{
|
||||
FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction,
|
||||
FighterStaleMoveQueue, HitStop,
|
||||
FighterAttackScriptAssets, FighterKnockbackState, FighterKnockbackVelocity,
|
||||
FighterPendingHitReaction, FighterStaleMoveQueue, HitStop,
|
||||
};
|
||||
use crate::fighter::attributes::FighterAttributes;
|
||||
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
|
||||
@@ -167,6 +167,7 @@ pub struct EnterContext<'a> {
|
||||
pub velocity: &'a mut FighterVelocity,
|
||||
pub translation: &'a mut FighterTranslation,
|
||||
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
|
||||
pub knockback_state: &'a mut FighterKnockbackState,
|
||||
pub stale_move_queue: &'a mut FighterStaleMoveQueue,
|
||||
pub facing_direction: &'a mut FighterFacingDirection,
|
||||
pub animation_frame: &'a mut FighterAnimationFrame,
|
||||
@@ -204,6 +205,8 @@ impl EnterContext<'_> {
|
||||
pub struct ExitContext<'a> {
|
||||
pub input: &'a mut FighterInput,
|
||||
pub hitboxes: &'a mut FighterHitboxes,
|
||||
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
|
||||
pub knockback_state: &'a mut FighterKnockbackState,
|
||||
pub surface: &'a mut FighterSurfaceState,
|
||||
pub vulnerability: &'a mut FighterInvulnerabilityState,
|
||||
}
|
||||
@@ -216,6 +219,7 @@ pub struct UpdateContext<'a> {
|
||||
pub velocity: &'a mut FighterVelocity,
|
||||
pub surface: &'a mut FighterSurfaceState,
|
||||
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
|
||||
pub knockback_state: &'a mut FighterKnockbackState,
|
||||
pub vulnerability: &'a mut FighterInvulnerabilityState,
|
||||
pub ledge_info: &'a mut FighterLedgeInformation,
|
||||
pub game_settings: &'a GameSettings,
|
||||
@@ -251,6 +255,8 @@ pub struct CollisionContext<'a> {
|
||||
pub prev_translation: &'a FighterPreviousTranslation,
|
||||
pub input: &'a FighterInput,
|
||||
pub velocity: &'a mut FighterVelocity,
|
||||
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
|
||||
pub knockback_state: &'a mut FighterKnockbackState,
|
||||
pub surface: &'a mut FighterSurfaceState,
|
||||
pub ecb: &'a mut FighterECB,
|
||||
pub desired_ecb: &'a mut FighterDesiredECB,
|
||||
@@ -335,6 +341,7 @@ pub struct FighterFrameQuery {
|
||||
pub velocity: &'static mut FighterVelocity,
|
||||
pub surface: &'static mut FighterSurfaceState,
|
||||
pub knockback_velocity: &'static mut FighterKnockbackVelocity,
|
||||
pub knockback_state: &'static mut FighterKnockbackState,
|
||||
pub stale_move_queue: &'static mut FighterStaleMoveQueue,
|
||||
pub ecb: &'static mut FighterECB,
|
||||
pub desired_ecb: &'static mut FighterDesiredECB,
|
||||
@@ -403,6 +410,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
hitboxes: &mut self.hitboxes,
|
||||
velocity: &mut self.velocity,
|
||||
knockback_velocity: &mut self.knockback_velocity,
|
||||
knockback_state: &mut self.knockback_state,
|
||||
translation: &mut self.translation,
|
||||
stale_move_queue: &mut self.stale_move_queue,
|
||||
facing_direction: &mut self.facing_direction,
|
||||
@@ -424,6 +432,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
ExitContext {
|
||||
input: &mut self.input,
|
||||
hitboxes: &mut self.hitboxes,
|
||||
knockback_velocity: &mut self.knockback_velocity,
|
||||
knockback_state: &mut self.knockback_state,
|
||||
surface: &mut self.surface,
|
||||
vulnerability: &mut self.vulnerability,
|
||||
}
|
||||
@@ -453,6 +463,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
velocity: &mut self.velocity,
|
||||
surface: &mut self.surface,
|
||||
knockback_velocity: &mut self.knockback_velocity,
|
||||
knockback_state: &mut self.knockback_state,
|
||||
game_settings,
|
||||
animation_root_motion_delta,
|
||||
animation_root_position,
|
||||
@@ -483,6 +494,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
presentation_snap: &mut self.presentation_snap,
|
||||
input: &self.input,
|
||||
velocity: &mut self.velocity,
|
||||
knockback_velocity: &mut self.knockback_velocity,
|
||||
knockback_state: &mut self.knockback_state,
|
||||
surface: &mut self.surface,
|
||||
ecb: &mut self.ecb,
|
||||
desired_ecb: &mut self.desired_ecb,
|
||||
@@ -655,9 +668,16 @@ pub fn state_interrupt_system(
|
||||
}
|
||||
|
||||
info!("Entering hitstop kind: {:?}", hitstop_kind);
|
||||
let stack_previous_knockback = !frame.surface.is_grounded()
|
||||
&& !frame.knockback_velocity.0.length_squared().is_zero()
|
||||
&& frame
|
||||
.knockback_state
|
||||
.can_stack(game_settings.fighter_common.knockback_stacking_window);
|
||||
|
||||
Some(FighterState::Hitstun(HitstunState::new(
|
||||
reaction,
|
||||
hitstop_kind,
|
||||
stack_previous_knockback,
|
||||
)))
|
||||
} else if hitstop.remaining_frames > 0 {
|
||||
None
|
||||
@@ -753,6 +773,8 @@ pub fn state_collision_interrupt_system(
|
||||
|
||||
// Clear hitstop
|
||||
hitstop.remaining_frames = 0;
|
||||
frame.knockback_velocity.0 = FGVec2::ZERO;
|
||||
frame.knockback_state.clear();
|
||||
|
||||
// subtract one stock
|
||||
|
||||
@@ -846,6 +868,8 @@ mod tests {
|
||||
let input = FighterInput::default();
|
||||
let mut translation = FighterTranslation::default();
|
||||
let mut velocity = FighterVelocity::default();
|
||||
let mut knockback_velocity = FighterKnockbackVelocity::default();
|
||||
let mut knockback_state = FighterKnockbackState::default();
|
||||
let mut surface = FighterSurfaceState::default();
|
||||
let mut ecb = FighterECB::default();
|
||||
let mut desired_ecb = FighterDesiredECB {
|
||||
@@ -865,6 +889,8 @@ mod tests {
|
||||
prev_translation: &previous_translation,
|
||||
input: &input,
|
||||
velocity: &mut velocity,
|
||||
knockback_velocity: &mut knockback_velocity,
|
||||
knockback_state: &mut knockback_state,
|
||||
surface: &mut surface,
|
||||
ecb: &mut ecb,
|
||||
desired_ecb: &mut desired_ecb,
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct FighterSettingsCommon {
|
||||
|
||||
/// Ground friction applied when the fighter is above the walk speed
|
||||
pub ground_friction_over_walk_speed_multiplier: FGi32,
|
||||
/// Minimum active launch frames between hits before airborne knockback stacks.
|
||||
pub knockback_stacking_window: u32,
|
||||
pub knockback_decay_per_frame: FGi32,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user