Implement post-match screen and match finishing.
CI / build-and-test (pull_request) Successful in 2m9s
CI / production-build (pull_request) Successful in 11m1s

This commit is contained in:
2026-09-28 19:16:21 +02:00
parent 3af36c4bc8
commit 68c3c512b0
5 changed files with 219 additions and 120 deletions
+1
View File
@@ -79,6 +79,7 @@ pub enum AppState {
LoadingMatch, LoadingMatch,
PreparingMatch, PreparingMatch,
InMatch, InMatch,
MatchResults,
MatchDisconnected, MatchDisconnected,
MatchLoadFailed, MatchLoadFailed,
} }
+52 -5
View File
@@ -49,6 +49,45 @@ impl MatchState {
.collect(), .collect(),
} }
} }
pub fn is_match_finished(&self, config: &MatchConfig) -> bool {
let time_limit = match config.rules {
MatchRules::Stock {
starting_stocks: _,
time_limit_frames,
} => time_limit_frames,
MatchRules::Timed { time_limit_frames } => Some(time_limit_frames),
};
if let Some(time_limit) = time_limit {
if self.elapsed_frames >= time_limit {
return true;
}
}
if let MatchRules::Stock {
starting_stocks: _,
time_limit_frames: _,
} = config.rules
{
let alive_fighter_count = self
.players
.iter()
.filter(|fighter| {
fighter
.stocks_remaining
.map(|stocks| stocks)
.unwrap_or_default()
> 0
})
.count();
if alive_fighter_count <= 1 {
return true;
}
}
false
}
} }
pub struct MatchRuntimePlugin; pub struct MatchRuntimePlugin;
@@ -63,7 +102,8 @@ impl Plugin for MatchRuntimePlugin {
.add_systems(OnExit(AppState::InMatch), reset_pause) .add_systems(OnExit(AppState::InMatch), reset_pause)
.add_systems( .add_systems(
PreUpdate, PreUpdate,
toggle_pause (toggle_pause, check_match_finished)
.chain()
.after(InputSystems) .after(InputSystems)
.before(RunGgrsSystems) .before(RunGgrsSystems)
.run_if(in_state(AppState::InMatch)), .run_if(in_state(AppState::InMatch)),
@@ -98,14 +138,21 @@ fn toggle_pause(
virtual_time.toggle(); virtual_time.toggle();
} }
fn reset_pause( fn reset_pause(mut pause: ResMut<PauseState>, mut virtual_time: ResMut<Time<Virtual>>) {
mut pause: ResMut<PauseState>,
mut virtual_time: ResMut<Time<Virtual>>,
) {
pause.paused = false; pause.paused = false;
virtual_time.unpause(); virtual_time.unpause();
} }
pub fn check_match_finished(
match_state: Res<MatchState>,
match_config: Res<MatchConfig>,
mut next_state: ResMut<NextState<AppState>>,
) {
if match_state.is_match_finished(&match_config) {
next_state.set(AppState::MatchResults);
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+44
View File
@@ -0,0 +1,44 @@
use bevy::{prelude::*, text::FontSourceTemplate};
use crate::{AppState, menus::{MenuMarker, spawn_menu}};
pub fn setup_post_match_menu(
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
commands: Commands,
) {
let menu = bsn! {
Node {
flex_direction: FlexDirection::Column,
width: percent(100),
height: percent(100),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center
}
Children [
(
Text("[Insert beautiful post-match screen here]")
TextFont {
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
font_size: px(super::style::H1_SIZE),
}
TextColor(Color::srgb(0.9, 0.9, 0.9))
),
Text("Press any button/key to continue...")
TextFont {
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
font_size: px(super::style::H3_SIZE),
}
TextColor(Color::srgb(0.9, 0.9, 0.9))
]
};
spawn_menu(existing_menu, commands, menu);
}
pub fn check_return_to_character_select(gamepads: Query<&Gamepad>, keyboard: Res<ButtonInput<KeyCode>>, mut next_state: ResMut<NextState<AppState>>) {
let any_pressed = gamepads.iter().any(|gp| gp.get_just_pressed().count() > 0);
let any_pressed = any_pressed || keyboard.get_just_pressed().count() > 0;
if any_pressed {
next_state.set(AppState::MainMenu);
}
}
+121 -114
View File
@@ -18,6 +18,7 @@ pub mod errors;
pub mod input; pub mod input;
pub mod main_menu; pub mod main_menu;
pub mod match_config; pub mod match_config;
pub mod match_results;
pub mod navigation; pub mod navigation;
pub mod scaling; pub mod scaling;
pub mod stage_select; pub mod stage_select;
@@ -77,11 +78,8 @@ pub fn despawn_menu(
mut commands: Commands, mut commands: Commands,
) { ) {
if let Some(entity) = existing_menu { if let Some(entity) = existing_menu {
commands commands.entity(entity.into_inner()).despawn();
.entity(entity.into_inner())
.despawn();
} }
} }
pub fn spawn_menu( pub fn spawn_menu(
@@ -90,9 +88,7 @@ pub fn spawn_menu(
menu: impl Scene, menu: impl Scene,
) { ) {
if let Some(entity) = existing_menu { if let Some(entity) = existing_menu {
commands commands.entity(entity.into_inner()).despawn();
.entity(entity.into_inner())
.despawn();
} }
commands.queue_spawn_scene(wrap_menu(menu)); commands.queue_spawn_scene(wrap_menu(menu));
} }
@@ -117,115 +113,126 @@ where
impl Plugin for MenuPlugin { impl Plugin for MenuPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems( app.add_systems(OnEnter(AppState::MainMenu), build_menu::<MainMenu>)
OnEnter(AppState::MainMenu), .add_message::<character_select::PlayerSlotAssigned>()
build_menu::<MainMenu>, .init_resource::<navigation::UINavigationActionState>()
) .init_resource::<navigation::UINavigationRepeat>()
.add_message::<character_select::PlayerSlotAssigned>() .init_resource::<input::MenuInputState>()
.init_resource::<navigation::UINavigationActionState>() .insert_resource(InputFocusVisible(true))
.init_resource::<navigation::UINavigationRepeat>() .insert_resource(GlobalUiDebugOptions { ..default() })
.init_resource::<input::MenuInputState>() // Configure auto-navigation behavior
.insert_resource(InputFocusVisible(true)) .insert_resource(AutoNavigationConfig {
.insert_resource(GlobalUiDebugOptions { // Require at least 10% overlap in perpendicular axis for cardinal directions
enabled: true, min_alignment_factor: 0.1,
..default() // Don't connect nodes more than 500 pixels apart between their closest edges
}) max_search_distance: Some(500.0),
// Configure auto-navigation behavior // Prefer nodes that are well-aligned
.insert_resource(AutoNavigationConfig { prefer_aligned: true,
// Require at least 10% overlap in perpendicular axis for cardinal directions })
min_alignment_factor: 0.1, .add_plugins(DirectionalNavigationPlugin)
// Don't connect nodes more than 500 pixels apart between their closest edges .add_systems(
max_search_distance: Some(500.0), PreUpdate,
// Prefer nodes that are well-aligned (
prefer_aligned: true, navigation::give_default_focus,
}) scaling::ui_scaling_system,
.add_plugins(DirectionalNavigationPlugin) button::button_setup,
.add_systems( button::button_style_system.after(bevy::picking::PickingSystems::Hover),
PreUpdate, button::button_focus_style_system,
( )
navigation::give_default_focus, .run_if(resource_exists::<CommonAssets>),
scaling::ui_scaling_system,
button::button_setup,
button::button_style_system.after(bevy::picking::PickingSystems::Hover),
button::button_focus_style_system,
) )
.run_if(resource_exists::<CommonAssets>), .add_systems(Startup, scaling::ui_update_scale_on_startup)
) // CURSOR
.add_systems(Startup, scaling::ui_update_scale_on_startup) .add_systems(
// CURSOR PreUpdate,
.add_systems( (cursor::cursor_input).before(bevy::picking::PickingSystems::ProcessInput),
PreUpdate,
(cursor::cursor_input).before(bevy::picking::PickingSystems::ProcessInput),
)
.add_systems(
Update,
(cursor::copy_mouse_input, cursor::update_cursor_transform),
)
.add_systems(
PreUpdate,
input::sample_menu_inputs
.before(cursor::cursor_input)
.run_if(resource_exists::<crate::input::MenuInputMap>),
)
//---------
// CHARACTER SELECT START
//---------
.add_systems(
OnEnter(AppState::CharacterSelect),
character_select::setup_character_select,
)
.add_systems(
PostUpdate,
character_select::copy_cursor_transform_to_token
.run_if(in_state(AppState::CharacterSelect))
.run_if(any_with_component::<CharacterSelectScreen>),
)
.add_systems(
PreUpdate,
(
character_select::css_auto_assign_slots,
character_select::handle_fighter_slot_addition,
) )
.chain() .add_systems(
.run_if(in_state(AppState::CharacterSelect)) Update,
.run_if(any_with_component::<CharacterSelectScreen>), (cursor::copy_mouse_input, cursor::update_cursor_transform),
)
.add_systems(
OnExit(AppState::CharacterSelect),
(cursor::despawn_cursors, character_select::despawn_tokens),
)
.add_observer(character_select::select_fighter.run_if(in_state(AppState::CharacterSelect)))
.add_observer(
character_select::update_start_banner_visibility
.run_if(in_state(AppState::CharacterSelect)),
)
//---------
// CHARACTER SELECT END
//---------
//---------
// STAGE SELECT START
//---------
.add_systems(
OnEnter(AppState::StageSelect),
stage_select::setup_stage_select,
)
.add_observer(stage_select::stage_selected.run_if(in_state(AppState::StageSelect)))
.add_systems(
OnExit(AppState::StageSelect),
(cursor::despawn_cursors, despawn_menu),
)
//---------
// STAGE SELECT END
//---------
.add_systems(
Update,
(
navigation::process_inputs,
navigation::navigate,
navigation::interact_with_focused_button,
) )
.chain(), .add_systems(
) PreUpdate,
.init_resource::<InputFocus>(); input::sample_menu_inputs
.before(cursor::cursor_input)
.run_if(resource_exists::<crate::input::MenuInputMap>),
)
//---------
// CHARACTER SELECT START
//---------
.add_systems(
OnEnter(AppState::CharacterSelect),
character_select::setup_character_select,
)
.add_systems(
PostUpdate,
character_select::copy_cursor_transform_to_token
.run_if(in_state(AppState::CharacterSelect))
.run_if(any_with_component::<CharacterSelectScreen>),
)
.add_systems(
PreUpdate,
(
character_select::css_auto_assign_slots,
character_select::handle_fighter_slot_addition,
)
.chain()
.run_if(in_state(AppState::CharacterSelect))
.run_if(any_with_component::<CharacterSelectScreen>),
)
.add_systems(
OnExit(AppState::CharacterSelect),
(cursor::despawn_cursors, character_select::despawn_tokens),
)
.add_observer(
character_select::select_fighter.run_if(in_state(AppState::CharacterSelect)),
)
.add_observer(
character_select::update_start_banner_visibility
.run_if(in_state(AppState::CharacterSelect)),
)
//---------
// CHARACTER SELECT END
//---------
//---------
// STAGE SELECT START
//---------
.add_systems(
OnEnter(AppState::StageSelect),
stage_select::setup_stage_select,
)
.add_observer(stage_select::stage_selected.run_if(in_state(AppState::StageSelect)))
.add_systems(
OnExit(AppState::StageSelect),
(cursor::despawn_cursors, despawn_menu),
)
//---------
// STAGE SELECT END
//---------
//---------
// MATCH RESULT START
//---------
.add_systems(
OnEnter(AppState::MatchResults),
match_results::setup_post_match_menu,
)
.add_systems(
PostUpdate,
match_results::check_return_to_character_select
.run_if(in_state(AppState::MatchResults)),
)
//---------
// MATCH RESULT END
//---------
.add_systems(
Update,
(
navigation::process_inputs,
navigation::navigate,
navigation::interact_with_focused_button,
)
.chain(),
)
.init_resource::<InputFocus>();
} }
} }
+1 -1
View File
@@ -102,7 +102,7 @@ pub fn process_inputs(
.insert(DirectionalNavigationAction::Right); .insert(DirectionalNavigationAction::Right);
} }
} }
if input.accept.is_pressed() { if input.accept.is_just_pressed() {
action_state action_state
.pressed_actions .pressed_actions
.insert(DirectionalNavigationAction::Select); .insert(DirectionalNavigationAction::Select);