diff --git a/src/fighter/attack/mod.rs b/src/fighter/attack/mod.rs index d1581e3..77dc69a 100644 --- a/src/fighter/attack/mod.rs +++ b/src/fighter/attack/mod.rs @@ -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, attack_scripts: Res>, ) { 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,8 +605,7 @@ 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); } } diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index cc792e0..0da2514 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -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>, game_settings: Res, fighter_manifests: Res>, @@ -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; }; diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 4cdb3e6..137ffbf 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -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 }, + Intangible { frames_remaining: Option } } 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; + } + } + } +} diff --git a/src/fighter/state/angel.rs b/src/fighter/state/angel.rs index e49a126..6d021ce 100644 --- a/src/fighter/state/angel.rs +++ b/src/fighter/state/angel.rs @@ -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( diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 9a56bd0..f4e2647 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -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 diff --git a/src/fighter/visual.rs b/src/fighter/visual.rs index f2be2a4..dc6e506 100644 --- a/src/fighter/visual.rs +++ b/src/fighter/visual.rs @@ -11,9 +11,27 @@ pub struct FighterAnimations { pub baked: Handle, } +#[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, ) { 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 diff --git a/src/game_settings.rs b/src/game_settings.rs index be52183..6c16cc7 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -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)]