Fix match state remaining mutable after and of match and implement initial work on aerials #77

Merged
EIREXE merged 4 commits from schedule_fix_end_of_match into master 2026-09-29 12:40:14 +00:00
2 changed files with 115 additions and 10 deletions
Showing only changes of commit edf2927d4d - Show all commits
+112 -10
View File
@@ -2,7 +2,7 @@
use bevy::input::InputSystems; use bevy::input::InputSystems;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ggrs::{RunGgrsSystems, prelude::*}; use bevy_ggrs::{ConfirmedFrameCount, RollbackFrameCount, RunGgrsSystems, prelude::*};
use crate::{ use crate::{
AppState, AppState,
@@ -15,6 +15,8 @@ pub struct MatchState {
pub elapsed_frames: u32, pub elapsed_frames: u32,
/// Indexed by the player's session handle. /// Indexed by the player's session handle.
pub players: Vec<PlayerMatchState>, pub players: Vec<PlayerMatchState>,
/// The rollback frame on which the match first finished.
pub finished_at_frame: Option<i32>,
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -47,6 +49,7 @@ impl MatchState {
deaths: 0, deaths: 0,
}) })
.collect(), .collect(),
finished_at_frame: None,
} }
} }
@@ -65,10 +68,11 @@ impl MatchState {
} }
} }
if let MatchRules::Stock { if config.players.len() > 1
starting_stocks: _, && let MatchRules::Stock {
time_limit_frames: _, starting_stocks: _,
} = config.rules time_limit_frames: _,
} = config.rules
{ {
let alive_fighter_count = self let alive_fighter_count = self
.players .players
@@ -88,6 +92,19 @@ impl MatchState {
false false
} }
fn advance_frame(&mut self, config: &MatchConfig, frame: i32) {
self.elapsed_frames = self.elapsed_frames.saturating_add(1);
if self.finished_at_frame.is_none() && self.is_match_finished(config) {
self.finished_at_frame = Some(frame);
}
}
fn finish_is_confirmed(&self, confirmed_frame: i32) -> bool {
self.finished_at_frame
.is_some_and(|finished_at| confirmed_frame >= finished_at)
}
} }
pub struct MatchRuntimePlugin; pub struct MatchRuntimePlugin;
@@ -100,13 +117,20 @@ impl Plugin for MatchRuntimePlugin {
.configure_sets(PreUpdate, RunGgrsSystems.run_if(ggrs_should_run)) .configure_sets(PreUpdate, RunGgrsSystems.run_if(ggrs_should_run))
.add_systems(OnEnter(AppState::InMatch), reset_pause) .add_systems(OnEnter(AppState::InMatch), reset_pause)
.add_systems(OnExit(AppState::InMatch), reset_pause) .add_systems(OnExit(AppState::InMatch), reset_pause)
.add_systems(
GgrsSchedule,
update_match_rules.in_set(crate::schedule::GameplaySet::MatchRules),
)
.add_systems( .add_systems(
PreUpdate, PreUpdate,
(toggle_pause, check_match_finished) toggle_pause
.chain()
.after(InputSystems) .after(InputSystems)
.before(RunGgrsSystems) .before(RunGgrsSystems)
.run_if(in_state(AppState::InMatch)), .run_if(in_state(AppState::InMatch)),
)
.add_systems(
Update,
transition_after_confirmed_finish.run_if(in_state(AppState::InMatch)),
); );
} }
} }
@@ -143,12 +167,20 @@ fn reset_pause(mut pause: ResMut<PauseState>, mut virtual_time: ResMut<Time<Virt
virtual_time.unpause(); virtual_time.unpause();
} }
pub fn check_match_finished( fn update_match_rules(
match_state: Res<MatchState>, mut match_state: ResMut<MatchState>,
match_config: Res<MatchConfig>, match_config: Res<MatchConfig>,
frame: Res<RollbackFrameCount>,
) {
match_state.advance_frame(&match_config, frame.0);
}
fn transition_after_confirmed_finish(
match_state: Res<MatchState>,
confirmed_frame: Res<ConfirmedFrameCount>,
mut next_state: ResMut<NextState<AppState>>, mut next_state: ResMut<NextState<AppState>>,
) { ) {
if match_state.is_match_finished(&match_config) { if match_state.finish_is_confirmed(confirmed_frame.0) {
next_state.set(AppState::MatchResults); next_state.set(AppState::MatchResults);
} }
} }
@@ -185,6 +217,7 @@ mod tests {
})); }));
assert_eq!(state.elapsed_frames, 0); assert_eq!(state.elapsed_frames, 0);
assert_eq!(state.finished_at_frame, None);
assert_eq!(state.players.len(), 2); assert_eq!(state.players.len(), 2);
assert!(state.players.iter().all(|player| { assert!(state.players.iter().all(|player| {
player.stocks_remaining == Some(4) && player.score == 0 && player.deaths == 0 player.stocks_remaining == Some(4) && player.score == 0 && player.deaths == 0
@@ -204,4 +237,73 @@ mod tests {
.all(|player| player.stocks_remaining.is_none()) .all(|player| player.stocks_remaining.is_none())
); );
} }
#[test]
fn timed_match_finishes_on_its_rollback_frame() {
let config = config(MatchRules::Timed {
time_limit_frames: 2,
});
let mut state = MatchState::from_config(&config);
state.advance_frame(&config, 10);
assert_eq!(state.elapsed_frames, 1);
assert_eq!(state.finished_at_frame, None);
state.advance_frame(&config, 11);
assert_eq!(state.elapsed_frames, 2);
assert_eq!(state.finished_at_frame, Some(11));
assert!(!state.finish_is_confirmed(10));
assert!(state.finish_is_confirmed(11));
state.advance_frame(&config, 12);
assert_eq!(state.finished_at_frame, Some(11));
}
#[test]
fn corrected_rollback_state_does_not_keep_speculative_finish() {
let config = config(MatchRules::Stock {
starting_stocks: 1,
time_limit_frames: None,
});
let state_before_prediction = MatchState::from_config(&config);
let mut predicted_state = state_before_prediction.clone();
predicted_state.players[1].stocks_remaining = Some(0);
predicted_state.advance_frame(&config, 20);
assert_eq!(predicted_state.finished_at_frame, Some(20));
let mut corrected_state = state_before_prediction;
corrected_state.advance_frame(&config, 20);
assert_eq!(corrected_state.finished_at_frame, None);
assert!(!corrected_state.finish_is_confirmed(20));
}
#[test]
fn single_player_stock_match_does_not_finish_by_elimination() {
let mut config = config(MatchRules::Stock {
starting_stocks: 1,
time_limit_frames: None,
});
config.players.truncate(1);
let mut state = MatchState::from_config(&config);
state.advance_frame(&config, 1);
assert_eq!(state.finished_at_frame, None);
state.players[0].stocks_remaining = Some(0);
state.advance_frame(&config, 2);
assert_eq!(state.finished_at_frame, None);
}
#[test]
fn single_player_stock_match_can_finish_by_time_limit() {
let mut config = config(MatchRules::Stock {
starting_stocks: 1,
time_limit_frames: Some(1),
});
config.players.truncate(1);
let mut state = MatchState::from_config(&config);
state.advance_frame(&config, 7);
assert_eq!(state.finished_at_frame, Some(7));
}
} }
+3
View File
@@ -25,6 +25,8 @@ pub enum GameplaySet {
Animation, Animation,
/// Resolve attacks /// Resolve attacks
Attacks, Attacks,
/// Advance match rules and detect the end of the match.
MatchRules,
} }
/// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by /// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by
@@ -43,6 +45,7 @@ impl Plugin for GameplaySchedulePlugin {
GameplaySet::Animation, GameplaySet::Animation,
GameplaySet::Collision, GameplaySet::Collision,
GameplaySet::Attacks, GameplaySet::Attacks,
GameplaySet::MatchRules,
) )
.chain(), .chain(),
); );