Implement invulnerability/intangibility and sakurai angle
CI / build-and-test (pull_request) Successful in 1m25s
CI / production-build (pull_request) Successful in 11m0s

This commit is contained in:
2026-10-03 11:58:38 +02:00
parent aca3886113
commit ebddf353d4
8 changed files with 170 additions and 44 deletions
+27 -4
View File
@@ -8,6 +8,7 @@ use bevy::prelude::*;
use bevy_ggrs::{Rollback, RollbackApp, prelude::GgrsSchedule};
use serde::{Deserialize, Serialize};
use crate::fighter::FighterInvulnerabilityState;
use crate::{
fighter::{
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes,
@@ -112,7 +113,7 @@ impl KnockbackType {
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub struct Knockback(FGi32);
impl Knockback {
@@ -157,6 +158,23 @@ impl AttackAngle {
pub fn make_sakurai() -> Self {
Self::Sakurai
}
pub fn get_angle(&self, knockback: &Knockback, is_receiver_grounded: bool, game_settings: &GameSettings) -> FGi32 {
match self {
AttackAngle::Normal(angle) => {
*angle
},
AttackAngle::Sakurai => {
if !is_receiver_grounded {
game_settings.fighter_common.sakurai_angle_aerial_angle
} else if knockback.0 > game_settings.fighter_common.sakurai_angle_knockback_threshold {
game_settings.fighter_common.sakurai_angle_high_knockback_angle
} else {
game_settings.fighter_common.sakurai_angle_low_knockback_angle
}
},
}
}
}
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
@@ -526,21 +544,27 @@ pub fn intersect_attacks_with_hurtboxes(
&Player,
&FighterStaleMoveQueue,
)>,
query_attacked: Query<(Entity, &FighterSolvedHurtboxes)>,
query_attacked: Query<(Entity, &FighterSolvedHurtboxes, &FighterInvulnerabilityState)>,
mut pending_hits: ResMut<FighterPendingHits>,
attack_scripts: Res<Assets<FighterAttackScript>>,
) {
pending_hits.0.clear();
for &entity_receiving in &roster.0 {
let Ok((_, hurtboxes)) = query_attacked.get(entity_receiving) else {
let Ok((_, hurtboxes, invuln_state)) = query_attacked.get(entity_receiving) else {
continue;
};
if matches!(*invuln_state, FighterInvulnerabilityState::Intangible { .. }) {
continue;
}
for &entity_hurting in &roster.0 {
if entity_hurting == entity_receiving {
continue;
}
let Ok((_, mut hitboxes, _, _, _)) = query_attacker.get_mut(entity_hurting) else {
continue;
};
@@ -581,7 +605,6 @@ pub fn intersect_attacks_with_hurtboxes(
attack_kind: active_attack.kind,
hitbox_index: hitting_hitbox_idx,
});
info!("Hit by hitbox {}!!!!", hitting_hitbox_idx);
active_attack.hit_fighters.push(entity_receiving);
}
+56 -17
View File
@@ -4,8 +4,16 @@ use bevy_ggrs::prelude::GgrsSchedule;
use super::*;
use crate::{
fighter::{
Fighter, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingHitReaction, FighterStaleMoveQueue, errors::FighterErrors, manifest::FighterManifest, state::FighterState,
}, game_settings::GameSettings, math::{int::FGi32}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript,
Fighter, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake,
FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingHitReaction,
FighterStaleMoveQueue, FighterSurfaceState, errors::FighterErrors,
manifest::FighterManifest, state::FighterState,
},
game_settings::GameSettings,
math::int::FGi32,
player::Player,
schedule::GameplaySet,
scripting::FighterAttackScript,
};
pub fn resolve_fighter_hits(
@@ -18,7 +26,14 @@ pub fn resolve_fighter_hits(
&mut FighterHitstop,
&mut FighterHitstopShake,
)>,
mut receiver: Query<(&mut FighterDamage, &Fighter, &mut FighterPendingHitReaction, &FighterState)>,
mut receiver: Query<(
&mut FighterDamage,
&Fighter,
&mut FighterPendingHitReaction,
&FighterState,
&FighterInvulnerabilityState,
&FighterSurfaceState,
)>,
attack_scripts: Res<Assets<FighterAttackScript>>,
game_settings: Res<GameSettings>,
fighter_manifests: Res<Assets<FighterManifest>>,
@@ -33,7 +48,14 @@ pub fn resolve_fighter_hits(
.position(|hit| hit.defender != defender)
.map_or(hits.len(), |offset| hit_offset + offset);
let Ok((mut receiver_damage, fighter, mut pending_reaction, receiver_state)) = receiver.get_mut(defender)
let Ok((
mut receiver_damage,
fighter,
mut pending_reaction,
receiver_state,
invuln_state,
receiver_surface_state,
)) = receiver.get_mut(defender)
else {
hit_offset = next_defender;
continue;
@@ -79,10 +101,12 @@ pub fn resolve_fighter_hits(
false,
false,
);
let angle = match &hitbox.angle {
AttackAngle::Normal(angle) => *angle,
AttackAngle::Sakurai => FGi32::lit("32.0"),
};
let angle = hitbox.angle.get_angle(
&knockback,
receiver_surface_state.is_grounded(),
&game_settings,
);
dbg!(knockback, angle);
let launch_angle = if facing_left {
FGi32::lit("180.0") - angle
} else {
@@ -100,15 +124,21 @@ pub fn resolve_fighter_hits(
hitstun_frames: knockback.get_hitstun_frames(&game_settings),
hitstop_frames: hitstop,
kind: HitReactionKind::Flinch,
damage: FighterDamage(attack_damage)
damage: FighterDamage(attack_damage),
},
));
}
receiver_damage.0 += total_damage_received_this_frame;
match invuln_state {
FighterInvulnerabilityState::Vulnerable => {
receiver_damage.0 += total_damage_received_this_frame
}
_ => (),
}
for (attacker_entity, _, reaction) in &reactions {
let Ok((.., _, mut hitstop, mut hitstop_shake)) = attacker.get_mut(*attacker_entity) else {
let Ok((.., _, mut hitstop, mut hitstop_shake)) = attacker.get_mut(*attacker_entity)
else {
continue;
};
@@ -119,7 +149,9 @@ pub fn resolve_fighter_hits(
FighterHitstopShakeKind::Vertical
};
hitstop.remaining_frames = reaction.hitstop_frames.0;
let shake_frames = reaction.hitstop_frames.calculate_shake_frames(&game_settings);
let shake_frames = reaction
.hitstop_frames
.calculate_shake_frames(&game_settings);
hitstop_shake.total_frames = shake_frames;
hitstop_shake.remaining_frames = shake_frames;
hitstop_shake.kind = shake_kind;
@@ -127,16 +159,23 @@ pub fn resolve_fighter_hits(
}
}
pending_reaction.0 = reactions
.into_iter()
.min_by(|(_, port_a, _), (_, port_b, _)| port_a.cmp(port_b))
.map(|(_, _, reaction)| reaction);
match invuln_state {
FighterInvulnerabilityState::Vulnerable => {
pending_reaction.0 = reactions
.into_iter()
.min_by(|(_, port_a, _), (_, port_b, _)| port_a.cmp(port_b))
.map(|(_, _, reaction)| reaction);
}
_ => (),
}
hit_offset = next_defender;
}
for hit in hits {
let Ok((mut stale_move_queue, _facing_direction, mut hitboxes, ..)) = attacker.get_mut(hit.attacker) else {
let Ok((mut stale_move_queue, _facing_direction, mut hitboxes, ..)) =
attacker.get_mut(hit.attacker)
else {
continue;
};
+35 -4
View File
@@ -85,12 +85,12 @@ pub struct FighterHitstop {
pub remaining_frames: u32,
}
#[derive(Component, Default, Copy, Clone, Hash, PartialEq, Eq)]
#[derive(Component, Default, Copy, Clone, Hash)]
pub enum FighterInvulnerabilityState {
#[default]
Vulnerable,
Invulnerable,
Intangible
Invulnerable { frames_remaining: Option<u32> },
Intangible { frames_remaining: Option<u32> }
}
impl FighterFacingDirection {
@@ -179,7 +179,7 @@ impl Plugin for FighterPlugin {
)
.add_systems(
GgrsSchedule,
(state::state_update_system, decay_hitstop)
(state::state_update_system, decay_hitstop, decay_invulnerability)
.chain()
.in_set(GameplaySet::StateUpdate),
)
@@ -360,3 +360,34 @@ pub fn decay_hitstop(query: Query<(&mut FighterHitstop, &mut FighterHitstopShake
hitstop_shake.remaining_frames = hitstop_shake.remaining_frames.saturating_sub(1);
}
}
pub fn decay_invulnerability(query: Query<&mut FighterInvulnerabilityState>) {
for invuln_state in query {
let invuln_state = invuln_state.into_inner();
let frames_remaining = match invuln_state {
FighterInvulnerabilityState::Vulnerable => None,
FighterInvulnerabilityState::Invulnerable { frames_remaining } => {
if let Some(frames_remaining) = frames_remaining {
*frames_remaining = frames_remaining.saturating_sub(1);
Some(*frames_remaining)
} else {
None
}
},
FighterInvulnerabilityState::Intangible { frames_remaining } => {
if let Some(frames_remaining) = frames_remaining {
*frames_remaining = frames_remaining.saturating_sub(1);
Some(*frames_remaining)
} else {
None
}
},
};
if let Some(frames_remaining) = frames_remaining {
if frames_remaining == 0 {
*invuln_state = FighterInvulnerabilityState::Vulnerable;
}
}
}
}
+3 -8
View File
@@ -33,14 +33,9 @@ impl FighterStateImpl for AirDodgeState {
}
fn update(&mut self, state_context: &mut UpdateContext) {
let airdodge_intangible_end = state_context.fighter_attributes.air_dodge_intangible_start
+ state_context
.fighter_attributes
.air_dodge_intangible_duration;
if state_context.animation_frame
== state_context.fighter_attributes.air_dodge_intangible_start
{
*state_context.vulnerability = FighterInvulnerabilityState::Intangible;
let airdodge_intangible_end = state_context.fighter_attributes.air_dodge_intangible_start + state_context.fighter_attributes.air_dodge_intangible_duration;
if state_context.animation_frame == state_context.fighter_attributes.air_dodge_intangible_start {
*state_context.vulnerability = FighterInvulnerabilityState::Intangible { frames_remaining: Some(state_context.fighter_attributes.air_dodge_intangible_duration) };
} else if state_context.animation_frame >= airdodge_intangible_end {
*state_context.vulnerability = FighterInvulnerabilityState::Vulnerable;
}
+2 -1
View File
@@ -1,4 +1,4 @@
use crate::{fighter::{animation::AnimKind, state::{FighterState, FighterStateImpl, air::AirborneStateCommon, fall::FallState}}, math::vec::FGVec2};
use crate::{fighter::{FighterInvulnerabilityState, animation::AnimKind, state::{FighterState, FighterStateImpl, air::AirborneStateCommon, fall::FallState}}, math::vec::FGVec2};
#[derive(Debug, Clone, Hash)]
pub struct AngelState {
@@ -38,6 +38,7 @@ impl FighterStateImpl for AngelState {
fn on_enter(&mut self, state_context: &mut super::EnterContext) {
state_context.play_animation(AnimKind::Wait, true);
state_context.velocity.0 = FGVec2::ZERO;
*state_context.vulnerability = FighterInvulnerabilityState::Invulnerable { frames_remaining: Some(state_context.game_settings.fighter_common.angel_platform_invulnerability_duration) };
}
fn check_collision_interrupt(
+7 -1
View File
@@ -161,6 +161,7 @@ pub struct EnterContext<'a> {
pub facing_direction: &'a mut FighterFacingDirection,
pub animation_frame: &'a mut FighterAnimationFrame,
pub surface: &'a mut FighterSurfaceState,
pub vulnerability: &'a mut FighterInvulnerabilityState,
pub fighter_attributes: &'a FighterAttributes,
pub attack_script_assets: &'a FighterAttackScriptAssets,
pub game_settings: &'a GameSettings,
@@ -329,6 +330,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
facing_direction: &mut self.facing_direction,
animation_frame: &mut self.animation_frame,
surface: &mut self.surface,
vulnerability: &mut self.vulnerability,
fighter_attributes: attributes,
attack_script_assets,
game_settings,
@@ -599,7 +601,11 @@ pub fn state_collision_interrupt_system(
if pending_respawn.0 {
**pending_respawn = false;
let respawn_state = FighterState::Angel(angel::AngelState::new(60 * 3));
let respawn_state = FighterState::Angel(angel::AngelState::new(
game_settings
.fighter_common
.angel_platform_invulnerability_duration,
));
// Find the angel platform position
let respawn_point = stage_manifest
+28 -8
View File
@@ -11,9 +11,27 @@ pub struct FighterAnimations {
pub baked: Handle<BakedFighterAnimations>,
}
#[derive(Clone, Copy, Default, PartialEq)]
pub enum LastKnownInvulnerabilityState {
#[default]
Vulnerable,
Intangible,
Invulnerable,
}
impl From<&FighterInvulnerabilityState> for LastKnownInvulnerabilityState {
fn from(value: &FighterInvulnerabilityState) -> Self {
match value {
FighterInvulnerabilityState::Vulnerable => Self::Vulnerable,
FighterInvulnerabilityState::Invulnerable { .. } => Self::Invulnerable,
FighterInvulnerabilityState::Intangible { .. } => Self::Intangible,
}
}
}
#[derive(Component, Clone, Default)]
pub struct FighterInvulnerabilityVisualState {
pub last_known_state: FighterInvulnerabilityState,
pub last_known_state: LastKnownInvulnerabilityState,
pub frames_in_state: u32,
}
@@ -27,8 +45,10 @@ pub fn update_status_colors(
game_settings: Res<GameSettings>,
) {
for (invuln_state, mut invuln_state_visual) in &mut fighters {
if *invuln_state != invuln_state_visual.last_known_state {
invuln_state_visual.last_known_state = *invuln_state;
let curr_state = invuln_state.into();
let last_known_state = invuln_state_visual.last_known_state;
if curr_state != last_known_state {
invuln_state_visual.last_known_state = curr_state;
invuln_state_visual.frames_in_state = 0;
}
}
@@ -38,18 +58,18 @@ pub fn update_status_colors(
.expect("Material should have relevant fighter available");
let status_color = match invuln_state_visual.last_known_state {
FighterInvulnerabilityState::Vulnerable => bevy::color::palettes::css::WHITE.into(),
FighterInvulnerabilityState::Invulnerable => {
LastKnownInvulnerabilityState::Vulnerable => bevy::color::palettes::css::WHITE.into(),
LastKnownInvulnerabilityState::Invulnerable => {
game_settings.fighter_common.intangible_color
}
FighterInvulnerabilityState::Intangible => {
LastKnownInvulnerabilityState::Intangible => {
game_settings.fighter_common.intangible_color
}
};
let intensity = match invuln_state_visual.last_known_state {
FighterInvulnerabilityState::Vulnerable => 0.0,
FighterInvulnerabilityState::Invulnerable | FighterInvulnerabilityState::Intangible => {
LastKnownInvulnerabilityState::Vulnerable => 0.0,
LastKnownInvulnerabilityState::Invulnerable | LastKnownInvulnerabilityState::Intangible => {
let cycle_frames = invuln_state_visual.frames_in_state
% game_settings.fighter_common.intangible_pulse_duration;
1.0 - cycle_frames as f32
+11
View File
@@ -45,6 +45,17 @@ pub struct FighterSettingsCommon {
pub intangible_color: Color,
pub intangible_pulse_duration: u32,
pub angel_platform_invulnerability_duration: u32,
/// When hit while the receiver is in the air, use this angle
pub sakurai_angle_aerial_angle: FGi32,
/// When hit while the receiver is in the ground and at low knockback, use this angle
pub sakurai_angle_low_knockback_angle: FGi32,
/// When hit while the receiver is in the ground and at high knockback, use this angle
pub sakurai_angle_high_knockback_angle: FGi32,
/// past this threshold, the knockback is considered high, so it will use [sakurai_angle_high_knockback_angle]
pub sakurai_angle_knockback_threshold: FGi32,
}
#[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)]