Add replay finished screen

This commit is contained in:
Sofia 2026-07-26 22:55:34 +03:00
parent 6cd8515449
commit e75cb79c86
7 changed files with 329 additions and 43 deletions

View File

@ -1,3 +1,6 @@
[gd_scene format=3 uid="uid://djmb2jqcpkuep"] [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] [node name="standalone_replay_global" type="StandaloneReplayManager" unique_id=730657724]
replay_finished_screen = ExtResource("1_q5ujf")

View File

@ -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

View File

@ -15,6 +15,7 @@ use crate::{
DamageSource, IPlayer, NetTransform, ball::Ball, remote_player::RemotePlayer, DamageSource, IPlayer, NetTransform, ball::Ball, remote_player::RemotePlayer,
weapon::WeaponType, weapon::WeaponType,
}, },
team_resource::TeamResource,
}; };
pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal"; pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal";
@ -22,9 +23,12 @@ pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal";
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Node, init)] #[class(base=Node, init)]
pub struct StandaloneReplayManager { pub struct StandaloneReplayManager {
#[export]
replay_finished_screen: Option<Gd<PackedScene>>,
loading_replay: bool, loading_replay: bool,
map: Option<Gd<Map>>, map: Option<Gd<Map>>,
playback: Option<Gd<ReplayPlayback>>, pub playback: Option<Gd<ReplayPlayback>>,
replay: Option<Replay>, replay: Option<Replay>,
base: Base<Node>, base: Base<Node>,
@ -63,6 +67,12 @@ impl StandaloneReplayManager {
if let Some(replay) = self.replay.take() { if let Some(replay) = self.replay.take() {
let mut playback = ReplayPlayback::new_alloc(); let mut playback = ReplayPlayback::new_alloc();
playback
.signals()
.on_finished()
.connect_other(self, Self::on_done);
playback.bind_mut().replay = replay; playback.bind_mut().replay = replay;
playback.bind_mut().current_map = Some(map); playback.bind_mut().current_map = Some(map);
playback.bind_mut().current_time = 0.; 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)] #[derive(GodotClass)]
@ -90,12 +114,22 @@ pub struct ReplayPlayback {
pub fast_forwarding: bool, pub fast_forwarding: bool,
pub finished: bool,
base: Base<Node>, base: Base<Node>,
} }
#[godot_api]
impl ReplayPlayback {
#[signal]
pub fn on_finished();
}
#[godot_api] #[godot_api]
impl INode for ReplayPlayback { impl INode for ReplayPlayback {
fn ready(&mut self) {} fn ready(&mut self) {
self.finished = false;
}
fn process(&mut self, _: f64) { fn process(&mut self, _: f64) {
if let Some(character) = self.player_characters.get_mut(&self.spectated_player_id) { 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; 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() { for (id, mut character) in self.player_characters.clone() {
character.queue_free(); character.queue_free();
self.player_characters.remove(&id); self.player_characters.remove(&id);
@ -234,6 +269,7 @@ impl INode for ReplayPlayback {
if let Some(mut ball) = self.ball.take() { if let Some(mut ball) = self.ball.take() {
ball.queue_free(); ball.queue_free();
} }
self.signals().on_finished().emit();
} }
} }
@ -277,11 +313,21 @@ impl INode for ReplayRecorder {
id: player.id(), id: player.id(),
name: player.data.name.clone(), name: player.data.name.clone(),
color: team.bind().color.into(), color: team.bind().color.into(),
team: player.data.team,
}, },
); );
} }
} }
s.replay.map_idx = game.bind().selected_map_idx; 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 ball_data = ReplayBall::Held;
let mut goals = HashMap::new();
if let Some(game) = Game::singleton() { if let Some(game) = Game::singleton() {
for player in &game.bind().players { for player in &game.bind().players {
if let Some(state) = player.get_state() { if let Some(state) = player.get_state() {
@ -338,10 +386,15 @@ impl ReplayRecorder {
} else { } else {
ReplayBall::Held ReplayBall::Held
}; };
goals = game.bind().goals.clone();
} }
self.replay.goals = goals.clone();
ReplayFrame { ReplayFrame {
time: self.time_elapsed, time: self.time_elapsed,
goals,
player_data, player_data,
ball_data, ball_data,
events: Vec::new(), events: Vec::new(),
@ -352,7 +405,9 @@ impl ReplayRecorder {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Replay { pub struct Replay {
pub map_idx: u8, pub map_idx: u8,
players: HashMap<u16, ReplayPlayer>, pub players: HashMap<u16, ReplayPlayer>,
pub teams: Vec<ReplayTeam>,
pub goals: HashMap<u8, u16>,
frames: Vec<ReplayFrame>, frames: Vec<ReplayFrame>,
} }
@ -361,16 +416,25 @@ impl Default for Replay {
Self { Self {
map_idx: Default::default(), map_idx: Default::default(),
players: Default::default(), players: Default::default(),
goals: Default::default(),
teams: Default::default(),
frames: Default::default(), frames: Default::default(),
} }
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayTeam {
pub name: String,
pub color: ReplayColor,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayPlayer { pub struct ReplayPlayer {
pub id: u16, pub id: u16,
pub name: String, pub name: String,
pub color: ReplayColor, pub color: ReplayColor,
pub team: u8,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -409,6 +473,7 @@ pub struct ReplayFrame {
player_data: HashMap<u16, ReplayPlayerState>, player_data: HashMap<u16, ReplayPlayerState>,
ball_data: ReplayBall, ball_data: ReplayBall,
events: Vec<ReplayEvent>, events: Vec<ReplayEvent>,
goals: HashMap<u8, u16>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -9,6 +9,7 @@ pub mod menu_player;
pub mod net_stats; pub mod net_stats;
pub mod player_listing; pub mod player_listing;
pub mod player_listings; pub mod player_listings;
pub mod replay_finished_screen;
pub mod settings; pub mod settings;
pub mod simple_score_view; pub mod simple_score_view;
pub mod slider; pub mod slider;

View File

@ -3,7 +3,7 @@ use godot::{
prelude::*, prelude::*,
}; };
use crate::game_manager::Game; use crate::{game_manager::Game, replay::StandaloneReplayManager};
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=HBoxContainer, init)] #[class(base=HBoxContainer, init)]
@ -53,45 +53,64 @@ impl IHBoxContainer for PlayerListing {
impl PlayerListing { impl PlayerListing {
pub fn update(&mut self) { pub fn update(&mut self) {
if let Some(game) = Game::singleton() { if let Some(player_id) = self.player_id {
if let Some(player_id) = self.player_id if let Some(game) = Game::singleton() {
&& let Some(player) = game.bind().find_player(player_id) if let Some(player) = game.bind().find_player(player_id) {
{ if let Some(name_label) = &mut self.name_label {
if let Some(name_label) = &mut self.name_label { name_label.set_text(&player.data.name);
name_label.set_text(&player.data.name); }
} if let Some(kills_label) = &mut self.kills_label {
if let Some(kills_label) = &mut self.kills_label { kills_label.set_text(&player.data.kills.to_string());
kills_label.set_text(&player.data.kills.to_string()); }
} if let Some(deaths_label) = &mut self.deaths_label {
if let Some(deaths_label) = &mut self.deaths_label { deaths_label.set_text(&player.data.deaths.to_string());
deaths_label.set_text(&player.data.deaths.to_string()); }
} if let Some(ping_label) = &mut self.ping_label {
if let Some(ping_label) = &mut self.ping_label { ping_label.set_text(&player.data.ping.to_string());
ping_label.set_text(&player.data.ping.to_string()); }
} if let Some(ping_icon) = &mut self.ping_icon {
if let Some(ping_icon) = &mut self.ping_icon { let (icon, color) = match player.data.ping {
let (icon, color) = match player.data.ping { ..100 => (self.ping_low.clone(), self.ping_low_color),
..100 => (self.ping_low.clone(), self.ping_low_color), 100..200 => (self.ping_medium.clone(), self.ping_medium_color),
100..200 => (self.ping_medium.clone(), self.ping_medium_color), 200.. => (self.ping_high.clone(), self.ping_high_color),
200.. => (self.ping_high.clone(), self.ping_high_color), };
}; if let Some(icon) = icon {
if let Some(icon) = icon { ping_icon.set_texture(&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 { } else if let Some(playback) = &StandaloneReplayManager::singleton().bind().playback {
if let Some(name_label) = &mut self.name_label { if let Some(player) = playback.bind().replay.players.get(&player_id) {
name_label.set_text("Name"); if let Some(name_label) = &mut self.name_label {
} name_label.set_text(&player.name);
if let Some(kills_label) = &mut self.kills_label { }
kills_label.set_text("Kills"); } else {
} if let Some(name_label) = &mut self.name_label {
if let Some(deaths_label) = &mut self.deaths_label { name_label.set_text("Name");
deaths_label.set_text("Deaths"); }
} if let Some(kills_label) = &mut self.kills_label {
if let Some(ping_label) = &mut self.ping_label { kills_label.set_text("Kills");
ping_label.set_text("Ping"); }
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");
}
} }
} }
} }

View File

@ -4,7 +4,9 @@ use godot::{
prelude::*, prelude::*,
}; };
use crate::{game_manager::Game, ui::player_listing::PlayerListing}; use crate::{
game_manager::Game, replay::StandaloneReplayManager, ui::player_listing::PlayerListing,
};
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=HBoxContainer, init)] #[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::<Vec<_>>();
let header = listing_scene.instantiate_as::<PlayerListing>();
vbox.add_child(&header);
for (_, player) in players {
let mut listing = listing_scene.instantiate_as::<PlayerListing>();
listing.bind_mut().player_id = Some(player.id);
vbox.add_child(&listing);
}
}
self.base_mut().add_child(&vbox); self.base_mut().add_child(&vbox);
} }
} }

View File

@ -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<Gd<HBoxContainer>>,
#[export]
back_button: Option<Gd<Button>>,
#[export]
player_listings: Option<Gd<PlayerListings>>,
base: Base<Panel>,
}
#[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::<Vec<_>>();
let winning_teams = playback
.replay
.teams
.iter()
.enumerate()
.filter(|(id, _)| winning_team_ids.contains(&((*id) as u8)))
.map(|(_, team)| team)
.collect::<Vec<_>>();
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);
}
}
}