diff --git a/godot/scenes/misc/standalone_replay_global.tscn b/godot/scenes/misc/standalone_replay_global.tscn index 1fbcbab..23f262a 100644 --- a/godot/scenes/misc/standalone_replay_global.tscn +++ b/godot/scenes/misc/standalone_replay_global.tscn @@ -1,3 +1,6 @@ [gd_scene format=3 uid="uid://djmb2jqcpkuep"] +[ext_resource type="PackedScene" uid="uid://d3377ecff6nyn" path="res://scenes/replay_finished.tscn" id="1_q5ujf"] + [node name="standalone_replay_global" type="StandaloneReplayManager" unique_id=730657724] +replay_finished_screen = ExtResource("1_q5ujf") diff --git a/godot/scenes/replay_finished.tscn b/godot/scenes/replay_finished.tscn new file mode 100644 index 0000000..2f4c6e7 --- /dev/null +++ b/godot/scenes/replay_finished.tscn @@ -0,0 +1,65 @@ +[gd_scene format=3 uid="uid://d3377ecff6nyn"] + +[ext_resource type="PackedScene" uid="uid://2sfy0vq0fqt3" path="res://scenes/ui/player_listing.tscn" id="1_k8op7"] + +[node name="replay_finished" type="ReplayFinishedScreen" unique_id=291257212 node_paths=PackedStringArray("title_container", "back_button", "player_listings")] +title_container = NodePath("title_container") +back_button = NodePath("button") +player_listings = NodePath("player_listings") +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="title_container" type="HBoxContainer" parent="." unique_id=980404596] +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -168.0 +offset_top = 55.0 +offset_right = 168.0 +offset_bottom = 110.0 +grow_horizontal = 2 +alignment = 1 + +[node name="button" type="Button" parent="." unique_id=1264024304] +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -40.0 +offset_top = -87.0 +offset_right = 42.0 +offset_bottom = -34.0 +grow_horizontal = 2 +grow_vertical = 0 +theme_override_font_sizes/font_size = 32 +text = "Back" + +[node name="player_listings" type="PlayerListings" parent="." unique_id=602626392] +player_listing_scene = ExtResource("1_k8op7") +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -88.0 +offset_top = 150.0 +offset_right = 87.5 +offset_bottom = 167.0 +grow_horizontal = 2 +alignment = 1 + +[node name="simple_score_view" type="SimpleScoreView" parent="." unique_id=1625972988] +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -20.0 +offset_top = 93.0 +offset_right = 20.0 +offset_bottom = 133.0 +grow_horizontal = 2 diff --git a/rust/src/replay.rs b/rust/src/replay.rs index c17e021..094ca3b 100644 --- a/rust/src/replay.rs +++ b/rust/src/replay.rs @@ -15,6 +15,7 @@ use crate::{ DamageSource, IPlayer, NetTransform, ball::Ball, remote_player::RemotePlayer, weapon::WeaponType, }, + team_resource::TeamResource, }; pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal"; @@ -22,9 +23,12 @@ pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal"; #[derive(GodotClass)] #[class(base=Node, init)] pub struct StandaloneReplayManager { + #[export] + replay_finished_screen: Option>, + loading_replay: bool, map: Option>, - playback: Option>, + pub playback: Option>, replay: Option, base: Base, @@ -63,6 +67,12 @@ impl StandaloneReplayManager { if let Some(replay) = self.replay.take() { let mut playback = ReplayPlayback::new_alloc(); + + playback + .signals() + .on_finished() + .connect_other(self, Self::on_done); + playback.bind_mut().replay = replay; playback.bind_mut().current_map = Some(map); playback.bind_mut().current_time = 0.; @@ -73,6 +83,20 @@ impl StandaloneReplayManager { } } } + + pub fn on_done(&mut self) { + if let Some(scene) = &self.replay_finished_screen { + self.base().get_tree().change_scene_to_packed(scene); + } + } + + pub fn clear(&mut self) { + self.replay.take(); + self.map.take(); + if let Some(mut playback) = self.playback.take() { + playback.queue_free(); + } + } } #[derive(GodotClass)] @@ -90,12 +114,22 @@ pub struct ReplayPlayback { pub fast_forwarding: bool, + pub finished: bool, + base: Base, } +#[godot_api] +impl ReplayPlayback { + #[signal] + pub fn on_finished(); +} + #[godot_api] impl INode for ReplayPlayback { - fn ready(&mut self) {} + fn ready(&mut self) { + self.finished = false; + } fn process(&mut self, _: f64) { if let Some(character) = self.player_characters.get_mut(&self.spectated_player_id) { @@ -226,7 +260,8 @@ impl INode for ReplayPlayback { self.fast_forwarding = false; - if self.current_frame_idx >= self.replay.frames.len() { + if !self.finished && self.current_frame_idx >= self.replay.frames.len() { + self.finished = true; for (id, mut character) in self.player_characters.clone() { character.queue_free(); self.player_characters.remove(&id); @@ -234,6 +269,7 @@ impl INode for ReplayPlayback { if let Some(mut ball) = self.ball.take() { ball.queue_free(); } + self.signals().on_finished().emit(); } } @@ -277,11 +313,21 @@ impl INode for ReplayRecorder { id: player.id(), name: player.data.name.clone(), color: team.bind().color.into(), + team: player.data.team, }, ); } } s.replay.map_idx = game.bind().selected_map_idx; + s.replay.teams = game + .bind() + .get_teams() + .iter_shared() + .map(|t| ReplayTeam { + name: t.bind().name.to_string(), + color: t.bind().color.into(), + }) + .collect(); } }); } @@ -324,6 +370,8 @@ impl ReplayRecorder { let mut ball_data = ReplayBall::Held; + let mut goals = HashMap::new(); + if let Some(game) = Game::singleton() { for player in &game.bind().players { if let Some(state) = player.get_state() { @@ -338,10 +386,15 @@ impl ReplayRecorder { } else { ReplayBall::Held }; + + goals = game.bind().goals.clone(); } + self.replay.goals = goals.clone(); + ReplayFrame { time: self.time_elapsed, + goals, player_data, ball_data, events: Vec::new(), @@ -352,7 +405,9 @@ impl ReplayRecorder { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Replay { pub map_idx: u8, - players: HashMap, + pub players: HashMap, + pub teams: Vec, + pub goals: HashMap, frames: Vec, } @@ -361,16 +416,25 @@ impl Default for Replay { Self { map_idx: Default::default(), players: Default::default(), + goals: Default::default(), + teams: Default::default(), frames: Default::default(), } } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReplayTeam { + pub name: String, + pub color: ReplayColor, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplayPlayer { pub id: u16, pub name: String, pub color: ReplayColor, + pub team: u8, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -409,6 +473,7 @@ pub struct ReplayFrame { player_data: HashMap, ball_data: ReplayBall, events: Vec, + goals: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/rust/src/ui/mod.rs b/rust/src/ui/mod.rs index 0542b62..3608953 100644 --- a/rust/src/ui/mod.rs +++ b/rust/src/ui/mod.rs @@ -9,6 +9,7 @@ pub mod menu_player; pub mod net_stats; pub mod player_listing; pub mod player_listings; +pub mod replay_finished_screen; pub mod settings; pub mod simple_score_view; pub mod slider; diff --git a/rust/src/ui/player_listing.rs b/rust/src/ui/player_listing.rs index 9990a95..e9cbd71 100644 --- a/rust/src/ui/player_listing.rs +++ b/rust/src/ui/player_listing.rs @@ -3,7 +3,7 @@ use godot::{ prelude::*, }; -use crate::game_manager::Game; +use crate::{game_manager::Game, replay::StandaloneReplayManager}; #[derive(GodotClass)] #[class(base=HBoxContainer, init)] @@ -53,45 +53,64 @@ impl IHBoxContainer for PlayerListing { impl PlayerListing { pub fn update(&mut self) { - if let Some(game) = Game::singleton() { - if let Some(player_id) = self.player_id - && let Some(player) = game.bind().find_player(player_id) - { - if let Some(name_label) = &mut self.name_label { - name_label.set_text(&player.data.name); - } - if let Some(kills_label) = &mut self.kills_label { - kills_label.set_text(&player.data.kills.to_string()); - } - if let Some(deaths_label) = &mut self.deaths_label { - deaths_label.set_text(&player.data.deaths.to_string()); - } - if let Some(ping_label) = &mut self.ping_label { - ping_label.set_text(&player.data.ping.to_string()); - } - if let Some(ping_icon) = &mut self.ping_icon { - let (icon, color) = match player.data.ping { - ..100 => (self.ping_low.clone(), self.ping_low_color), - 100..200 => (self.ping_medium.clone(), self.ping_medium_color), - 200.. => (self.ping_high.clone(), self.ping_high_color), - }; - if let Some(icon) = icon { - ping_icon.set_texture(&icon); + if let Some(player_id) = self.player_id { + if let Some(game) = Game::singleton() { + if let Some(player) = game.bind().find_player(player_id) { + if let Some(name_label) = &mut self.name_label { + name_label.set_text(&player.data.name); + } + if let Some(kills_label) = &mut self.kills_label { + kills_label.set_text(&player.data.kills.to_string()); + } + if let Some(deaths_label) = &mut self.deaths_label { + deaths_label.set_text(&player.data.deaths.to_string()); + } + if let Some(ping_label) = &mut self.ping_label { + ping_label.set_text(&player.data.ping.to_string()); + } + if let Some(ping_icon) = &mut self.ping_icon { + let (icon, color) = match player.data.ping { + ..100 => (self.ping_low.clone(), self.ping_low_color), + 100..200 => (self.ping_medium.clone(), self.ping_medium_color), + 200.. => (self.ping_high.clone(), self.ping_high_color), + }; + if let Some(icon) = icon { + ping_icon.set_texture(&icon); + } + ping_icon.set_modulate(color); + } + } else { + if let Some(name_label) = &mut self.name_label { + name_label.set_text("Name"); + } + if let Some(kills_label) = &mut self.kills_label { + kills_label.set_text("Kills"); + } + if let Some(deaths_label) = &mut self.deaths_label { + deaths_label.set_text("Deaths"); + } + if let Some(ping_label) = &mut self.ping_label { + ping_label.set_text("Ping"); } - ping_icon.set_modulate(color); } - } else { - if let Some(name_label) = &mut self.name_label { - name_label.set_text("Name"); - } - if let Some(kills_label) = &mut self.kills_label { - kills_label.set_text("Kills"); - } - if let Some(deaths_label) = &mut self.deaths_label { - deaths_label.set_text("Deaths"); - } - if let Some(ping_label) = &mut self.ping_label { - ping_label.set_text("Ping"); + } else if let Some(playback) = &StandaloneReplayManager::singleton().bind().playback { + if let Some(player) = playback.bind().replay.players.get(&player_id) { + if let Some(name_label) = &mut self.name_label { + name_label.set_text(&player.name); + } + } else { + if let Some(name_label) = &mut self.name_label { + name_label.set_text("Name"); + } + if let Some(kills_label) = &mut self.kills_label { + kills_label.set_text("Kills"); + } + if let Some(deaths_label) = &mut self.deaths_label { + deaths_label.set_text("Deaths"); + } + if let Some(ping_label) = &mut self.ping_label { + ping_label.set_text("Ping"); + } } } } diff --git a/rust/src/ui/player_listings.rs b/rust/src/ui/player_listings.rs index 6b3e301..4748c4b 100644 --- a/rust/src/ui/player_listings.rs +++ b/rust/src/ui/player_listings.rs @@ -4,7 +4,9 @@ use godot::{ prelude::*, }; -use crate::{game_manager::Game, ui::player_listing::PlayerListing}; +use crate::{ + game_manager::Game, replay::StandaloneReplayManager, ui::player_listing::PlayerListing, +}; #[derive(GodotClass)] #[class(base=HBoxContainer, init)] @@ -69,6 +71,40 @@ impl PlayerListings { } } + self.base_mut().add_child(&vbox); + } + } else if let Some(playback) = &StandaloneReplayManager::singleton().bind().playback { + let replay = &playback.bind().replay; + + for (id, team) in replay.teams.iter().enumerate() { + let mut vbox = VBoxContainer::new_alloc(); + + vbox.set_alignment(AlignmentMode::BEGIN); + vbox.set_custom_minimum_size(Vector2::new(300., 0.)); + + let mut team_label = Label::new_alloc(); + team_label.set_modulate(team.color.clone().into()); + team_label.set_text(&team.name); + team_label.set_horizontal_alignment(HorizontalAlignment::CENTER); + vbox.add_child(&team_label); + + if let Some(listing_scene) = &self.player_listing_scene { + let players = replay + .players + .iter() + .filter(|(_, p)| p.team == id as u8) + .collect::>(); + + let header = listing_scene.instantiate_as::(); + vbox.add_child(&header); + + for (_, player) in players { + let mut listing = listing_scene.instantiate_as::(); + listing.bind_mut().player_id = Some(player.id); + vbox.add_child(&listing); + } + } + self.base_mut().add_child(&vbox); } } diff --git a/rust/src/ui/replay_finished_screen.rs b/rust/src/ui/replay_finished_screen.rs new file mode 100644 index 0000000..72b4a3d --- /dev/null +++ b/rust/src/ui/replay_finished_screen.rs @@ -0,0 +1,97 @@ +use godot::{ + classes::{Button, FileDialog, HBoxContainer, IPanel, Label, Panel}, + prelude::*, +}; + +use crate::{ + game_manager::{Game, GameManager}, + replay::StandaloneReplayManager, + ui::player_listings::PlayerListings, +}; + +#[derive(GodotClass)] +#[class(base=Panel, init)] +pub struct ReplayFinishedScreen { + #[export] + title_container: Option>, + #[export] + back_button: Option>, + #[export] + player_listings: Option>, + + base: Base, +} + +#[godot_api] +impl IPanel for ReplayFinishedScreen { + fn ready(&mut self) { + self.produce_title(); + if let Some(listings) = &mut self.player_listings { + listings.bind_mut().update_player_listings(); + } + if let Some(back) = &self.back_button { + back.signals().pressed().connect_other(self, |_| { + StandaloneReplayManager::singleton().bind_mut().clear(); + GameManager::singleton().bind().go_to_main_menu(); + }); + } + } +} + +#[godot_api] +impl ReplayFinishedScreen { + fn produce_title(&mut self) { + if let Some(playback) = &StandaloneReplayManager::singleton().bind().playback { + let playback = playback.bind(); + godot_print!("{:?}", playback.replay.goals); + let max_goals = playback + .replay + .goals + .iter() + .map(|(_, v)| v) + .max() + .copied() + .unwrap_or(0); + let winning_team_ids = playback + .replay + .goals + .iter() + .filter(|(_, goals)| **goals == max_goals) + .map(|(team, _)| *team) + .collect::>(); + let winning_teams = playback + .replay + .teams + .iter() + .enumerate() + .filter(|(id, _)| winning_team_ids.contains(&((*id) as u8))) + .map(|(_, team)| team) + .collect::>(); + + if let Some(container) = &mut self.title_container { + for (idx, team) in winning_teams.iter().enumerate() { + let mut label = Label::new_alloc(); + label.set_text(&team.name); + label.set_modulate(team.color.clone().into()); + if idx > 0 { + let mut and_label = Label::new_alloc(); + and_label.set_text(" and "); + container.add_child(&and_label); + } + container.add_child(&label); + } + let mut end = Label::new_alloc(); + end.set_text(" victory!"); + container.add_child(&end); + } + } + } + + pub fn save_replay(&mut self, path: GString) { + if let Some(game) = &Game::singleton() + && let Some(recorder) = &game.bind().replay_recorder + { + recorder.bind().save_to(&path); + } + } +}