Add pulsing effect when invulnerable/intangible, make airdodge make the player intangible. (#75)
CI / build-and-test (push) Successful in 2m3s
CI / production-build (push) Successful in 15m42s

Reviewed-on: #75
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #75.
This commit is contained in:
2026-09-28 15:44:50 +00:00
committed by EIREXE
parent ccf2657d29
commit 3af36c4bc8
14 changed files with 311 additions and 43 deletions
+3
View File
@@ -44,6 +44,9 @@ pub struct FighterAttributes {
pub air_dodge_duration: u32,
pub air_dodge_velocity: FGi32,
pub air_dodge_decay: FGi32,
pub air_dodge_intangible_duration: u32,
pub air_dodge_intangible_start: u32,
}
impl FighterAttributes {
+18 -3
View File
@@ -41,8 +41,8 @@ use crate::{
AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage,
FighterKnockbackVelocity, FighterPendingHitReaction, FighterSolvedHurtboxes,
FighterStaleMoveQueue,
}, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, ecb::FighterECBBoneIndices, manifest::FighterManifest, state::{FighterState, air::AirborneStateCommon, fall::FallState}, visual::FighterAnimations,
}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision,
}, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, ecb::FighterECBBoneIndices, manifest::FighterManifest, state::{FighterState, air::AirborneStateCommon, fall::FallState}, visual::{FighterAnimations, FighterInvulnerabilityVisualState},
}, game_settings::GameSettings, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision,
};
use state::state_interrupt_system;
@@ -85,6 +85,14 @@ pub struct FighterHitstop {
pub remaining_frames: u32,
}
#[derive(Component, Default, Copy, Clone, Hash, PartialEq, Eq)]
pub enum FighterInvulnerabilityState {
#[default]
Vulnerable,
Invulnerable,
Intangible
}
impl FighterFacingDirection {
fn reverse(&self) -> Self {
match self {
@@ -140,7 +148,9 @@ pub struct FighterPendingRespawn(bool);
FighterPendingRespawn,
FighterHitstop,
FighterKnockbackVelocity,
FighterTranslation
FighterTranslation,
FighterInvulnerabilityState,
FighterInvulnerabilityVisualState,
)]
pub struct Fighter {
pub manifest: Handle<FighterManifest>,
@@ -229,6 +239,8 @@ impl Plugin for FighterPlugin {
.checksum_component_with_hash::<FighterPendingRespawn>()
.rollback_component_with_copy::<FighterHitstop>()
.checksum_component_with_hash::<FighterHitstop>()
.rollback_component_with_copy::<FighterInvulnerabilityState>()
.checksum_component_with_hash::<FighterInvulnerabilityState>()
.rollback_component_with_copy::<FighterKnockbackVelocity>()
.checksum_component_with_hash::<FighterKnockbackVelocity>()
.rollback_resource_with_reflect::<StageCollision>()
@@ -256,6 +268,9 @@ impl Plugin for FighterPlugin {
.run_if(crate::debug_tools::attack_hitboxes_enabled)
.run_if(in_state(AppState::InMatch)),
)
.add_systems(FixedPostUpdate,
visual::update_status_colors.run_if(resource_exists::<GameSettings>)
)
.add_systems(
FixedPostUpdate,
debug::hurtbox_debug.run_if(crate::debug_tools::hurtboxes_enabled),
+2
View File
@@ -169,6 +169,8 @@ mod tests {
air_dodge_duration: 0,
air_dodge_velocity: FGi32::ZERO,
air_dodge_decay: FGi32::ZERO,
air_dodge_intangible_duration: 29,
air_dodge_intangible_start: 4,
}
}
+16 -5
View File
@@ -1,16 +1,13 @@
use crate::{
fighter::{
animation::AnimKind,
state::{
FighterInvulnerabilityState, animation::AnimKind, state::{
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext,
UpdateContext,
air::{self, AirborneStateCommon},
fall::FallState,
land::LandingType,
},
},
input,
math::vec::FGVec2,
}, input, math::vec::FGVec2,
};
#[derive(Debug, Clone, Hash)]
@@ -32,6 +29,12 @@ 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;
} else if state_context.animation_frame >= airdodge_intangible_end {
*state_context.vulnerability = FighterInvulnerabilityState::Vulnerable;
}
state_context.velocity.0 *= state_context.fighter_attributes.air_dodge_decay;
air::apply_air_motion(
@@ -62,6 +65,14 @@ impl FighterStateImpl for AirDodgeState {
fn surface_policy(&self) -> super::SurfacePolicy {
super::SurfacePolicy::Airborne(crate::fighter::surface::EcbLockPolicy::StartOnDetach)
}
fn on_exit(&mut self, state_context: &mut super::ExitContext) {
*state_context.vulnerability = FighterInvulnerabilityState::Vulnerable;
}
fn name(&self) -> String {
Self::NAME.to_owned()
}
}
pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
+6 -4
View File
@@ -18,10 +18,7 @@ use crate::fighter::state::hitstun::HitstunState;
use crate::fighter::surface::EcbLockPolicy;
use crate::fighter::visual::FighterAnimations;
use crate::fighter::{
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingRespawn,
FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation,
FighterVelocity, FighterDamage,
Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterInvulnerabilityState, FighterPendingRespawn, FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity,
};
use crate::game_settings::GameSettings;
use crate::input::FighterInput;
@@ -173,6 +170,7 @@ impl EnterContext<'_> {
pub struct ExitContext<'a> {
pub hitboxes: &'a mut FighterHitboxes,
pub surface: &'a mut FighterSurfaceState,
pub vulnerability: &'a mut FighterInvulnerabilityState,
}
pub struct UpdateContext<'a> {
@@ -183,6 +181,7 @@ pub struct UpdateContext<'a> {
pub velocity: &'a mut FighterVelocity,
pub surface: &'a mut FighterSurfaceState,
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
pub vulnerability: &'a mut FighterInvulnerabilityState,
pub game_settings: &'a GameSettings,
pub animation_frame: u32,
pub fighter_attributes: &'a FighterAttributes,
@@ -265,6 +264,7 @@ pub struct FighterFrameQuery {
pub prev_ecb: &'static FighterPreviousECB,
pub facing_direction: &'static mut FighterFacingDirection,
pub animation_frame: &'static mut FighterAnimationFrame,
pub vulnerability: &'static mut FighterInvulnerabilityState,
pub fighter: &'static Fighter,
}
@@ -330,6 +330,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
ExitContext {
hitboxes: &mut self.hitboxes,
surface: &mut self.surface,
vulnerability: &mut self.vulnerability,
}
}
@@ -350,6 +351,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
knockback_velocity: &mut self.knockback_velocity,
game_settings,
animation_frame: self.animation_frame.frame,
vulnerability: &mut self.vulnerability,
fighter_attributes: attributes,
attack_scripts,
attack_script_assets,
+64 -1
View File
@@ -1,4 +1,8 @@
use crate::fighter::baked_animation::BakedFighterAnimations;
use crate::{
fighter::{FighterInvulnerabilityState, baked_animation::BakedFighterAnimations},
game_settings::GameSettings,
materials::fighter_material::{FighterMaterial, FighterMesh},
};
use bevy::prelude::*;
#[derive(Component, Clone)]
@@ -6,3 +10,62 @@ pub struct FighterAnimations {
/// The single animation source for gameplay and the visual skeleton.
pub baked: Handle<BakedFighterAnimations>,
}
#[derive(Component, Clone, Default)]
pub struct FighterInvulnerabilityVisualState {
pub last_known_state: FighterInvulnerabilityState,
pub frames_in_state: u32,
}
pub fn update_status_colors(
mut fighters: Query<(
&FighterInvulnerabilityState,
&mut FighterInvulnerabilityVisualState,
)>,
meshes: Query<(&FighterMesh, &MeshMaterial3d<FighterMaterial>)>,
mut materials: ResMut<Assets<FighterMaterial>>,
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;
invuln_state_visual.frames_in_state = 0;
}
}
for (mesh, mat) in meshes {
let (_, invuln_state_visual) = fighters
.get(mesh.fighter)
.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 => {
game_settings.fighter_common.intangible_color
}
FighterInvulnerabilityState::Intangible => {
game_settings.fighter_common.intangible_color
}
};
let intensity = match invuln_state_visual.last_known_state {
FighterInvulnerabilityState::Vulnerable => 0.0,
FighterInvulnerabilityState::Invulnerable | FighterInvulnerabilityState::Intangible => {
let cycle_frames = invuln_state_visual.frames_in_state
% game_settings.fighter_common.intangible_pulse_duration;
1.0 - cycle_frames as f32
/ ((game_settings.fighter_common.intangible_pulse_duration) as f32)
}
};
let mut material = materials
.get_mut(mat.id())
.expect("Material should exist here...");
material
.extension
.update_status_color(status_color.into(), intensity);
}
for (_, mut invuln_state_visual) in fighters {
invuln_state_visual.frames_in_state = invuln_state_visual.frames_in_state.saturating_add(1);
}
}