From 68c3c512b0d9cefa15ebe7611264514f9482408e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Mon, 28 Sep 2026 19:16:21 +0200 Subject: [PATCH] Implement post-match screen and match finishing. --- src/lib.rs | 1 + src/match_runtime.rs | 57 +++++++- src/menus/match_results/mod.rs | 44 ++++++ src/menus/mod.rs | 235 +++++++++++++++++---------------- src/menus/navigation.rs | 2 +- 5 files changed, 219 insertions(+), 120 deletions(-) create mode 100644 src/menus/match_results/mod.rs diff --git a/src/lib.rs b/src/lib.rs index eebc28d..88cb8d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,7 @@ pub enum AppState { LoadingMatch, PreparingMatch, InMatch, + MatchResults, MatchDisconnected, MatchLoadFailed, } diff --git a/src/match_runtime.rs b/src/match_runtime.rs index 5e8bca0..b66f7bb 100644 --- a/src/match_runtime.rs +++ b/src/match_runtime.rs @@ -49,6 +49,45 @@ impl MatchState { .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; @@ -63,7 +102,8 @@ impl Plugin for MatchRuntimePlugin { .add_systems(OnExit(AppState::InMatch), reset_pause) .add_systems( PreUpdate, - toggle_pause + (toggle_pause, check_match_finished) + .chain() .after(InputSystems) .before(RunGgrsSystems) .run_if(in_state(AppState::InMatch)), @@ -98,14 +138,21 @@ fn toggle_pause( virtual_time.toggle(); } -fn reset_pause( - mut pause: ResMut, - mut virtual_time: ResMut>, -) { +fn reset_pause(mut pause: ResMut, mut virtual_time: ResMut>) { pause.paused = false; virtual_time.unpause(); } +pub fn check_match_finished( + match_state: Res, + match_config: Res, + mut next_state: ResMut>, +) { + if match_state.is_match_finished(&match_config) { + next_state.set(AppState::MatchResults); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/menus/match_results/mod.rs b/src/menus/match_results/mod.rs new file mode 100644 index 0000000..54b27b4 --- /dev/null +++ b/src/menus/match_results/mod.rs @@ -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>>, + 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>, mut next_state: ResMut>) { + 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); + } +} \ No newline at end of file diff --git a/src/menus/mod.rs b/src/menus/mod.rs index 3976631..c223284 100644 --- a/src/menus/mod.rs +++ b/src/menus/mod.rs @@ -18,6 +18,7 @@ pub mod errors; pub mod input; pub mod main_menu; pub mod match_config; +pub mod match_results; pub mod navigation; pub mod scaling; pub mod stage_select; @@ -77,11 +78,8 @@ pub fn despawn_menu( mut commands: Commands, ) { if let Some(entity) = existing_menu { - commands - .entity(entity.into_inner()) - .despawn(); + commands.entity(entity.into_inner()).despawn(); } - } pub fn spawn_menu( @@ -90,9 +88,7 @@ pub fn spawn_menu( menu: impl Scene, ) { if let Some(entity) = existing_menu { - commands - .entity(entity.into_inner()) - .despawn(); + commands.entity(entity.into_inner()).despawn(); } commands.queue_spawn_scene(wrap_menu(menu)); } @@ -117,115 +113,126 @@ where impl Plugin for MenuPlugin { fn build(&self, app: &mut App) { - app.add_systems( - OnEnter(AppState::MainMenu), - build_menu::, - ) - .add_message::() - .init_resource::() - .init_resource::() - .init_resource::() - .insert_resource(InputFocusVisible(true)) - .insert_resource(GlobalUiDebugOptions { - enabled: true, - ..default() - }) - // Configure auto-navigation behavior - .insert_resource(AutoNavigationConfig { - // Require at least 10% overlap in perpendicular axis for cardinal directions - min_alignment_factor: 0.1, - // Don't connect nodes more than 500 pixels apart between their closest edges - max_search_distance: Some(500.0), - // Prefer nodes that are well-aligned - prefer_aligned: true, - }) - .add_plugins(DirectionalNavigationPlugin) - .add_systems( - PreUpdate, - ( - navigation::give_default_focus, - scaling::ui_scaling_system, - button::button_setup, - button::button_style_system.after(bevy::picking::PickingSystems::Hover), - button::button_focus_style_system, + app.add_systems(OnEnter(AppState::MainMenu), build_menu::) + .add_message::() + .init_resource::() + .init_resource::() + .init_resource::() + .insert_resource(InputFocusVisible(true)) + .insert_resource(GlobalUiDebugOptions { ..default() }) + // Configure auto-navigation behavior + .insert_resource(AutoNavigationConfig { + // Require at least 10% overlap in perpendicular axis for cardinal directions + min_alignment_factor: 0.1, + // Don't connect nodes more than 500 pixels apart between their closest edges + max_search_distance: Some(500.0), + // Prefer nodes that are well-aligned + prefer_aligned: true, + }) + .add_plugins(DirectionalNavigationPlugin) + .add_systems( + PreUpdate, + ( + navigation::give_default_focus, + 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::), ) - .run_if(resource_exists::), - ) - .add_systems(Startup, scaling::ui_update_scale_on_startup) - // CURSOR - .add_systems( - 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::), - ) - //--------- - // 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::), - ) - .add_systems( - PreUpdate, - ( - character_select::css_auto_assign_slots, - character_select::handle_fighter_slot_addition, + .add_systems(Startup, scaling::ui_update_scale_on_startup) + // CURSOR + .add_systems( + PreUpdate, + (cursor::cursor_input).before(bevy::picking::PickingSystems::ProcessInput), ) - .chain() - .run_if(in_state(AppState::CharacterSelect)) - .run_if(any_with_component::), - ) - .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, + .add_systems( + Update, + (cursor::copy_mouse_input, cursor::update_cursor_transform), ) - .chain(), - ) - .init_resource::(); + .add_systems( + PreUpdate, + input::sample_menu_inputs + .before(cursor::cursor_input) + .run_if(resource_exists::), + ) + //--------- + // 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::), + ) + .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::), + ) + .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::(); } } diff --git a/src/menus/navigation.rs b/src/menus/navigation.rs index 2ca7a5f..70739f2 100644 --- a/src/menus/navigation.rs +++ b/src/menus/navigation.rs @@ -102,7 +102,7 @@ pub fn process_inputs( .insert(DirectionalNavigationAction::Right); } } - if input.accept.is_pressed() { + if input.accept.is_just_pressed() { action_state .pressed_actions .insert(DirectionalNavigationAction::Select);