//! Rollback-simulated state for an active match. use bevy::prelude::*; use bevy_ggrs::prelude::*; use crate::match_loading::{MatchConfig, MatchRules}; #[derive(Resource, Clone, Debug, Hash, PartialEq, Eq)] pub struct MatchState { pub elapsed_frames: u32, /// Indexed by the player's session handle. pub players: Vec, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct PlayerMatchState { pub stocks_remaining: Option, pub score: i32, pub deaths: u32, } impl MatchState { pub fn from_config(config: &MatchConfig) -> Self { let stocks_remaining = match &config.rules { MatchRules::Stock { starting_stocks, .. } => Some(*starting_stocks), MatchRules::Timed { .. } => None, }; Self { elapsed_frames: 0, players: (0..config.players.len()) .map(|_| PlayerMatchState { stocks_remaining, score: 0, deaths: 0, }) .collect(), } } } pub struct MatchRuntimePlugin; impl Plugin for MatchRuntimePlugin { fn build(&self, app: &mut App) { app.rollback_resource_with_clone::() .checksum_resource_with_hash::(); } } #[cfg(test)] mod tests { use super::*; use crate::{fighter::FighterId, match_loading::MatchPlayer, stage::manifest::StageId}; fn config(rules: MatchRules) -> MatchConfig { MatchConfig { stage: StageId::TestStage, players: vec![ MatchPlayer { handle: 1, fighter: FighterId::TestFighter, }, MatchPlayer { handle: 0, fighter: FighterId::TestFighter, }, ], rules, } } #[test] fn stock_state_starts_with_configured_stocks() { let state = MatchState::from_config(&config(MatchRules::Stock { starting_stocks: 4, time_limit_frames: Some(60), })); assert_eq!(state.elapsed_frames, 0); assert_eq!(state.players.len(), 2); assert!(state.players.iter().all(|player| { player.stocks_remaining == Some(4) && player.score == 0 && player.deaths == 0 })); } #[test] fn timed_state_has_no_stock_counter() { let state = MatchState::from_config(&config(MatchRules::Timed { time_limit_frames: 60, })); assert!( state .players .iter() .all(|player| player.stocks_remaining.is_none()) ); } }