Implement invulnerability/intangibility and sakurai angle (#78)
Reviewed-on: #78 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #78.
This commit is contained in:
@@ -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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user