//! Everything fighter-shaped: the components that describe a fighter, the //! state machine that drives it, and the plugin that schedules it all. use bevy::prelude::*; use bevy_egui::EguiPrimaryContextPass; use bevy_ggrs::prelude::*; use serde::{Deserialize, Serialize}; pub mod animation; pub mod attack; pub mod attributes; pub mod baked_animation; pub mod collision; pub mod debug; pub mod ecb; pub mod errors; pub mod hurtbox; pub mod manifest; pub mod motion; pub mod push; pub mod state; pub mod surface; pub mod visual; pub mod blast_zones; // The fighter components are re-exported so the rest of the crate can say // `fighter::FighterVelocity` without caring which file it lives in. Named // explicitly rather than globbed, so moving a type between submodules can't // silently change what this module exports. pub use attributes::FighterAttributes; pub use ecb::FighterECB; pub use manifest::FighterId; pub use motion::{ FighterPresentationSnap, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, }; pub use surface::FighterSurfaceState; use crate::{ AppState, fighter::{ attack::{ AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterSolvedHurtboxes, FighterStaleMoveQueue, }, baked_animation::FighterBoneIndexMap, 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, }; use state::state_interrupt_system; /// Owns fighter simulation, the rollback registrations for fighter /// components, and the fighter debug overlays. #[derive(Default)] pub struct FighterPlugin; #[derive(Component, Copy, Clone, Hash, Debug, PartialEq, Reflect)] pub enum FighterFacingDirection { Left, Right, } #[derive(Component, Copy, Clone, Hash, Debug, PartialEq, Reflect, Default)] pub enum FighterHitstopShakeKind { #[default] Horizontal, Vertical, } #[derive(Component, Copy, Clone, Hash, Debug, PartialEq, Reflect, Default)] pub enum FighterHitstopShakeRole { #[default] Attacker, Receiver, } #[derive(Component, Default, Copy, Clone, Hash)] pub struct FighterHitstopShake { pub remaining_frames: u32, pub total_frames: u32, pub kind: FighterHitstopShakeKind, pub role: FighterHitstopShakeRole, } #[derive(Component, Default, Copy, Clone, Hash)] #[require(FighterHitstopShake)] pub struct FighterHitstop { pub remaining_frames: u32, } #[derive(Component, Default, Copy, Clone, Hash, Debug, PartialEq, Eq)] pub struct FighterLedgeInformation { pub regrab_lockout_remaining_frames: u32, pub grab_count: u32, } impl FighterLedgeInformation { pub fn start(&mut self, duration_frames: u32) { self.regrab_lockout_remaining_frames = duration_frames; } pub fn is_regrab_lockout_active(&self) -> bool { self.regrab_lockout_remaining_frames > 0 } fn tick(&mut self) { self.regrab_lockout_remaining_frames = self.regrab_lockout_remaining_frames.saturating_sub(1); } } #[cfg(test)] mod ledge_grab_lockout_tests { use super::FighterLedgeInformation; #[test] fn lockout_expires_after_the_requested_number_of_ticks() { let mut lockout = FighterLedgeInformation::default(); lockout.start(2); assert!(lockout.is_regrab_lockout_active()); lockout.tick(); assert!(lockout.is_regrab_lockout_active()); lockout.tick(); assert!(!lockout.is_regrab_lockout_active()); } #[test] fn zero_duration_does_not_enable_lockout() { let mut lockout = FighterLedgeInformation::default(); lockout.start(0); assert!(!lockout.is_regrab_lockout_active()); } } #[derive(Component, Default, Copy, Clone, Hash)] pub enum FighterInvulnerabilityState { #[default] Vulnerable, Invulnerable { frames_remaining: Option }, Intangible { frames_remaining: Option } } impl FighterFacingDirection { fn reverse(&self) -> Self { match self { FighterFacingDirection::Left => FighterFacingDirection::Right, FighterFacingDirection::Right => FighterFacingDirection::Left, } } } #[derive(Copy, Clone, Debug, Reflect, Serialize, Deserialize)] #[reflect(opaque)] pub struct FighterCameraProfile { pub vertical_origin_offset: FGi32, pub forward_extent: FGi32, pub backward_extent: FGi32, pub upward_extent: FGi32, pub downward_extent: FGi32, /// Reserved for future offscreen indicators and close-up camera modes. pub visibility_radius: FGi32, } impl FighterCameraProfile { pub fn is_valid(&self) -> bool { self.forward_extent >= FGi32::ZERO && self.backward_extent >= FGi32::ZERO && self.upward_extent >= FGi32::ZERO && self.downward_extent >= FGi32::ZERO && self.visibility_radius >= FGi32::ZERO } } impl FighterFacingDirection { pub fn to_sign(&self) -> FGi32 { match self { FighterFacingDirection::Left => FGi32::NEG_ONE, FighterFacingDirection::Right => FGi32::ONE, } } } #[derive(Component)] #[require(Transform)] pub struct FighterVisual; #[derive(Component, DerefMut, Deref, Debug, Default, Hash, Clone, Copy)] pub struct FighterPendingRespawn(bool); #[derive(Component)] #[require( FighterStaleMoveQueue, FighterDamage, FighterPendingHitReaction, FighterPendingRespawn, FighterHitstop, FighterLedgeInformation, FighterKnockbackVelocity, FighterTranslation, FighterInvulnerabilityState, FighterInvulnerabilityVisualState, )] pub struct Fighter { pub manifest: Handle, } #[derive(Resource, Clone, Debug)] pub struct FighterRoster(pub Vec); impl Plugin for FighterPlugin { fn build(&self, app: &mut App) { app.add_plugins(FighterAttackPlugin) // Every state gets a chance to interrupt itself, in priority order. .add_systems( GgrsSchedule, motion::clear_fighter_presentation_snaps.in_set(GameplaySet::Input), ) .add_systems( GgrsSchedule, ( (ecb::snapshot_fighter_ecb, motion::snapshot_fighter_motion) .before(state_interrupt_system), animation::advance_fighter_animation_frames.before(state_interrupt_system), state::state_interrupt_system, ) .in_set(GameplaySet::Interrupt), ) .add_systems( GgrsSchedule, (state::state_update_system, decay_hitstop, decay_invulnerability) .chain() .in_set(GameplaySet::StateUpdate), ) /*.add_systems( GgrsSchedule, (motion::integrate_gravity, motion::apply_grounded_motion) .chain() .in_set(GameplaySet::Physics), )*/ .add_systems( GgrsSchedule, ( ( blast_zones::apply_blast_zones, ecb::update_ecb, ecb::decrement_ecb_lockout, state::state_collision_interrupt_system, decay_ledge_grab_lockout, ).chain(), baked_animation::resample_fighter_bone_matrices, ) .chain() .in_set(GameplaySet::Collision), ) .add_systems( GgrsSchedule, push::apply_grounded_pushes .before(state::state_update_system) .in_set(GameplaySet::StateUpdate), ) .add_systems( PostUpdate, ( motion::apply_fighter_translation_to_visuals, animation::apply_animation, ) .chain() .before(bevy::transform::TransformSystems::Propagate) .run_if(in_state(crate::AppState::InMatch)), ) // Rollback registration lives next to the systems that write these // components; a component simulated here but missing from this list // is a desync waiting to happen. .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_clone::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_clone::() .checksum_component_with_hash::() .rollback_component_with_copy::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_clone::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_clone::() .checksum_component_with_hash::() // Debug views. .add_systems( FixedPostUpdate, debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled), ) .add_systems( FixedPostUpdate, debug::ledge_grab_debug.run_if(crate::debug_tools::ledge_grab_enabled), ) .add_systems( EguiPrimaryContextPass, debug::fighter_debug.run_if(crate::debug_tools::fighter_info_enabled), ) .add_systems( FixedPostUpdate, debug::animation_debug.run_if(crate::debug_tools::animation_bones_enabled), ) .add_systems( FixedPostUpdate, ( debug::attack_debug_gather, (debug::attack_debug, debug::attack_launch_debug_draw) .after(debug::attack_debug_gather), ) .chain() .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::) ) .add_systems( FixedPostUpdate, debug::hurtbox_debug.run_if(crate::debug_tools::hurtboxes_enabled), ) .rollback_component_with_clone::() .rollback_component_with_clone::() .rollback_component_with_clone::() .checksum_component_with_hash::() .checksum_component_with_hash::() .checksum_component_with_hash::() .register_type::(); } } #[derive(Clone, Debug, Hash)] pub struct FighterActiveAttack { pub kind: AttackKind, pub script: Handle, pub active_hitboxes: Vec, pub solved_hitboxes: Vec, pub hit_groups: Vec, pub stalled: bool, } #[derive(Clone, Debug, Hash)] pub struct FighterHitGroupState { pub group: u32, pub hit_fighters: Vec, } #[derive(Component, Clone, Debug, Default, Hash)] pub struct FighterHitboxes(pub Option); pub fn spawn_fighter( commands: &mut Commands, player_handle: usize, controller_port: usize, spawn_position: FGVec2, manifest: Handle, model_scale: FGi32, animations: FighterAnimations, attack_scripts: FighterAttackScriptAssets, visual_root: WorldAssetRoot, ecb_bone_indices: Vec, ) -> Entity { commands .spawn(( Name(format!("Fighter {player_handle}").into()), Player { handle: player_handle, controller_port, }, Fighter { manifest: manifest }, FighterFacingDirection::Right, FighterPreviousFacingDirection(FighterFacingDirection::Right), FighterSurfaceState::default(), Transform::from_scale(Vec3::splat(model_scale.to_num())), ( FighterECB::default(), FighterHitboxes::default(), FighterECBBoneIndices { indices: ecb_bone_indices } ), ( FighterVelocity::default(), FighterTranslation(spawn_position), FighterPreviousTranslation(spawn_position), FighterPresentationSnap::default(), ), visual_root, animations, attack_scripts, FighterState::Fall(FallState::new(AirborneStateCommon::default())), animation::FighterAnimationFrame::new(animation::AnimKind::Wait, true), ( baked_animation::FighterBoneMatrices::default(), FighterBoneIndexMap::default(), FighterSolvedHurtboxes::default(), FighterVisual, FighterInput::default(), crate::camera::FighterCameraExtents::default(), ), )) .id() } pub fn decay_hitstop(query: Query<(&mut FighterHitstop, &mut FighterHitstopShake)>) { for (mut hitstop, mut hitstop_shake) in query { hitstop.remaining_frames = hitstop.remaining_frames.saturating_sub(1); hitstop_shake.remaining_frames = hitstop_shake.remaining_frames.saturating_sub(1); } } pub fn decay_ledge_grab_lockout(query: Query<&mut FighterLedgeInformation>) { for mut lockout in query { lockout.tick(); } } 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; } } } }