363 lines
14 KiB
Rust
363 lines
14 KiB
Rust
//! 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 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, 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;
|
|
|
|
/// 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, PartialEq, Eq)]
|
|
pub enum FighterInvulnerabilityState {
|
|
#[default]
|
|
Vulnerable,
|
|
Invulnerable,
|
|
Intangible
|
|
}
|
|
|
|
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,
|
|
FighterKnockbackVelocity,
|
|
FighterTranslation,
|
|
FighterInvulnerabilityState,
|
|
FighterInvulnerabilityVisualState,
|
|
)]
|
|
pub struct Fighter {
|
|
pub manifest: Handle<FighterManifest>,
|
|
}
|
|
|
|
#[derive(Resource, Clone, Debug)]
|
|
pub struct FighterRoster(pub Vec<Entity>);
|
|
|
|
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)
|
|
.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).chain(),
|
|
baked_animation::resample_fighter_bone_matrices,
|
|
)
|
|
.chain()
|
|
.in_set(GameplaySet::Collision),
|
|
)
|
|
.add_systems(
|
|
PostUpdate,
|
|
(
|
|
motion::apply_fighter_translation_to_visuals,
|
|
animation::apply_animation,
|
|
)
|
|
.chain()
|
|
.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::<FighterECB>()
|
|
.checksum_component_with_hash::<FighterECB>()
|
|
.rollback_component_with_clone::<FighterStaleMoveQueue>()
|
|
.checksum_component_with_hash::<FighterStaleMoveQueue>()
|
|
.rollback_component_with_copy::<ecb::FighterPreviousECB>()
|
|
.checksum_component_with_hash::<ecb::FighterPreviousECB>()
|
|
.rollback_component_with_copy::<FighterVelocity>()
|
|
.checksum_component_with_hash::<FighterVelocity>()
|
|
.rollback_component_with_copy::<FighterTranslation>()
|
|
.checksum_component_with_hash::<FighterTranslation>()
|
|
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
|
.checksum_component_with_hash::<FighterPreviousTranslation>()
|
|
.rollback_component_with_copy::<FighterPresentationSnap>()
|
|
.checksum_component_with_hash::<FighterPresentationSnap>()
|
|
.rollback_component_with_copy::<FighterPreviousFacingDirection>()
|
|
.checksum_component_with_hash::<FighterPreviousFacingDirection>()
|
|
.rollback_component_with_copy::<animation::FighterAnimationFrame>()
|
|
.checksum_component_with_hash::<animation::FighterAnimationFrame>()
|
|
.rollback_component_with_clone::<FighterSurfaceState>()
|
|
.checksum_component_with_hash::<FighterSurfaceState>()
|
|
.rollback_component_with_copy::<FighterFacingDirection>()
|
|
.rollback_component_with_copy::<FighterDamage>()
|
|
.checksum_component_with_hash::<FighterDamage>()
|
|
.rollback_component_with_clone::<FighterPendingHitReaction>()
|
|
.checksum_component_with_hash::<FighterPendingHitReaction>()
|
|
.rollback_component_with_copy::<FighterPendingRespawn>()
|
|
.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>()
|
|
// Debug views.
|
|
.add_systems(
|
|
FixedPostUpdate,
|
|
debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_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::<GameSettings>)
|
|
)
|
|
.add_systems(
|
|
FixedPostUpdate,
|
|
debug::hurtbox_debug.run_if(crate::debug_tools::hurtboxes_enabled),
|
|
)
|
|
.rollback_component_with_clone::<FighterState>()
|
|
.rollback_component_with_clone::<FighterHitboxes>()
|
|
.rollback_component_with_clone::<FighterBoneMatrices>()
|
|
.rollback_component_with_clone::<FighterHitstopShake>()
|
|
.checksum_component_with_hash::<FighterHitboxes>()
|
|
.rollback_component_with_clone::<FighterSolvedHurtboxes>()
|
|
.checksum_component_with_hash::<FighterSolvedHurtboxes>()
|
|
.checksum_component_with_hash::<FighterState>()
|
|
.checksum_component_with_hash::<FighterFacingDirection>()
|
|
.register_type::<FighterFacingDirection>();
|
|
}
|
|
}
|
|
#[derive(Clone, Debug, Hash)]
|
|
pub struct FighterActiveAttack {
|
|
pub kind: AttackKind,
|
|
pub script: Handle<FighterAttackScript>,
|
|
pub active_hitboxes: Vec<usize>,
|
|
pub solved_hitboxes: Vec<attack::FighterSolvedHitbox>,
|
|
pub hit_fighters: Vec<Entity>,
|
|
pub stalled: bool,
|
|
}
|
|
|
|
#[derive(Component, Clone, Debug, Default, Hash)]
|
|
pub struct FighterHitboxes(pub Option<FighterActiveAttack>);
|
|
|
|
pub fn spawn_fighter(
|
|
commands: &mut Commands,
|
|
player_handle: usize,
|
|
controller_port: usize,
|
|
spawn_position: FGVec2,
|
|
manifest: Handle<FighterManifest>,
|
|
model_scale: FGi32,
|
|
animations: FighterAnimations,
|
|
attack_scripts: FighterAttackScriptAssets,
|
|
visual_root: WorldAssetRoot,
|
|
ecb_bone_indices: Vec<usize>,
|
|
) -> 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);
|
|
}
|
|
}
|