use bevy::prelude::*; use crate::{ camera::MatchCamera, fighter::{ FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded, animation::FighterAnimationFrame, attack::{ FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits, FighterSolvedHurtboxes, FighterStaleMoveQueue, }, baked_animation::FighterBoneMatrices, state::FighterState, }, input::FighterInput, player::Player, stage::line::StageCollision, }; #[derive(Resource, Default)] pub(super) struct DebugSaveState { checkpoint: Option, } #[derive(Clone)] struct Checkpoint { fighters: Vec, stage_collision: StageCollision, pending_hits: FighterPendingHits, } #[derive(Clone)] struct FighterCheckpoint { entity: Entity, handle: usize, ecb: FighterECB, previous_ecb: crate::fighter::ecb::FighterPreviousECB, velocity: FighterVelocity, translation: FighterTranslation, previous_translation: FighterPreviousTranslation, facing: FighterFacingDirection, previous_facing: FighterPreviousFacingDirection, animation: FighterAnimationFrame, state: FighterState, input: FighterInput, damage: FighterDamage, pending_hit_reaction: FighterPendingHitReaction, hitstop: FighterHitstop, knockback_velocity: FighterKnockbackVelocity, stale_moves: FighterStaleMoveQueue, hitboxes: FighterHitboxes, solved_hurtboxes: FighterSolvedHurtboxes, bone_matrices: FighterBoneMatrices, grounded: bool, } pub(super) fn save_or_restore(world: &mut World) { if world.resource::().menu_open { return; } let left = world .query::<&Gamepad>() .iter(world) .any(|gamepad| gamepad.just_pressed(GamepadButton::DPadLeft)); let right = world .query::<&Gamepad>() .iter(world) .any(|gamepad| gamepad.just_pressed(GamepadButton::DPadRight)); if left == right { return; } let Some(roster) = world .get_resource::() .map(|roster| roster.0.clone()) else { return; }; if roster.len() != 1 { warn!("Save states are only available in single-player matches"); return; } if left { let Some(stage_collision) = world.get_resource::().cloned() else { return; }; let mut checkpoint_fighters = Vec::with_capacity(roster.len()); for entity in roster.iter().copied() { let Some(checkpoint) = capture_fighter(world, entity) else { warn!("Could not save the current fighter state"); return; }; checkpoint_fighters.push(checkpoint); } let pending_hits = world.resource::().clone(); world.resource_mut::().checkpoint = Some(Checkpoint { fighters: checkpoint_fighters, stage_collision, pending_hits, }); info!("Saved debug game state"); return; } let Some(checkpoint) = world.resource::().checkpoint.clone() else { warn!("No debug game state has been saved"); return; }; if checkpoint.fighters.len() != roster.len() || checkpoint .fighters .iter() .zip(roster.iter()) .any(|(saved, current)| saved.entity != *current) { warn!("Saved debug game state belongs to another match"); return; } for saved in &checkpoint.fighters { let Some(player) = world.entity(saved.entity).get::() else { warn!("Could not restore the saved fighter state"); return; }; if player.handle != saved.handle { warn!("Saved debug game state belongs to another match"); return; } let mut entity = world.entity_mut(saved.entity); entity.insert(saved.ecb); entity.insert(saved.previous_ecb); entity.insert(saved.velocity); entity.insert(saved.translation); entity.insert(saved.previous_translation); entity.insert(saved.facing); entity.insert(saved.previous_facing); entity.insert(saved.animation); entity.insert(saved.state.clone()); entity.insert(saved.input); entity.insert(saved.damage); entity.insert(saved.pending_hit_reaction.clone()); entity.insert(saved.hitstop); entity.insert(saved.knockback_velocity); entity.insert(saved.stale_moves.clone()); entity.insert(saved.hitboxes.clone()); entity.insert(saved.solved_hurtboxes.clone()); entity.insert(saved.bone_matrices.clone()); if saved.grounded { entity.insert(Grounded); } else { entity.remove::(); } } world.insert_resource(checkpoint.pending_hits.clone()); world.insert_resource(checkpoint.stage_collision); for mut match_camera in world.query::<&mut MatchCamera>().iter_mut(world) { match_camera.needs_snap = true; } info!("Restored debug game state"); } fn capture_fighter(world: &World, entity: Entity) -> Option { let fighter = world.entity(entity); Some(FighterCheckpoint { entity, handle: fighter.get::()?.handle, ecb: *fighter.get::()?, previous_ecb: *fighter.get::()?, velocity: *fighter.get::()?, translation: *fighter.get::()?, previous_translation: *fighter.get::()?, facing: *fighter.get::()?, previous_facing: *fighter.get::()?, animation: *fighter.get::()?, state: fighter.get::()?.clone(), input: *fighter.get::()?, damage: *fighter.get::()?, pending_hit_reaction: fighter.get::()?.clone(), hitstop: *fighter.get::()?, knockback_velocity: *fighter.get::()?, stale_moves: fighter.get::()?.clone(), hitboxes: fighter.get::()?.clone(), solved_hurtboxes: fighter.get::()?.clone(), bone_matrices: fighter.get::()?.clone(), grounded: fighter.contains::(), }) } pub(super) fn clear(mut save_state: ResMut) { save_state.checkpoint = None; }