Compare commits

..

3 Commits

Author SHA1 Message Date
d99850129d Add some player stats 2026-08-02 23:13:39 +03:00
3ccc17ac3b Record goals, own_goals, friendly_kills and friendly_deaths 2026-08-02 21:54:08 +03:00
7db901c749 Move some stats to PlayerStats 2026-08-02 21:49:19 +03:00
10 changed files with 220 additions and 27 deletions

View File

@ -2,6 +2,7 @@
[ext_resource type="Theme" uid="uid://drydbnrcdv4pk" path="res://default_theme.tres" id="1_6xxma"] [ext_resource type="Theme" uid="uid://drydbnrcdv4pk" path="res://default_theme.tres" id="1_6xxma"]
[ext_resource type="PackedScene" uid="uid://2sfy0vq0fqt3" path="res://scenes/ui/player_listing.tscn" id="1_ou354"] [ext_resource type="PackedScene" uid="uid://2sfy0vq0fqt3" path="res://scenes/ui/player_listing.tscn" id="1_ou354"]
[ext_resource type="PackedScene" uid="uid://thi85gv3blli" path="res://scenes/ui/player_stats_panel.tscn" id="3_5t2yd"]
[node name="game_finished" type="GameFinishedScreen" unique_id=700430082 node_paths=PackedStringArray("title_container", "back_button", "player_listings", "save_file_dialog")] [node name="game_finished" type="GameFinishedScreen" unique_id=700430082 node_paths=PackedStringArray("title_container", "back_button", "player_listings", "save_file_dialog")]
title_container = NodePath("title_container") title_container = NodePath("title_container")
@ -59,8 +60,9 @@ grow_vertical = 0
theme_override_font_sizes/font_size = 32 theme_override_font_sizes/font_size = 32
text = "Save Replay" text = "Save Replay"
[node name="player_listings" type="PlayerListings" parent="." unique_id=602626392] [node name="player_listings" type="PlayerListings" parent="." unique_id=602626392 node_paths=PackedStringArray("player_stats_panel")]
player_listing_scene = ExtResource("1_ou354") player_listing_scene = ExtResource("1_ou354")
player_stats_panel = NodePath("../player_stats_panel")
layout_mode = 1 layout_mode = 1
anchors_preset = 5 anchors_preset = 5
anchor_left = 0.5 anchor_left = 0.5
@ -87,4 +89,11 @@ grow_horizontal = 2
access = 2 access = 2
filters = PackedStringArray("*.replay") filters = PackedStringArray("*.replay")
[node name="player_stats_panel" parent="." unique_id=820294448 instance=ExtResource("3_5t2yd")]
layout_mode = 1
offset_left = -52.0
offset_top = -36.0
offset_right = 52.0
offset_bottom = 36.0
[connection signal="pressed" from="save_replay" to="." method="open_file_dialog"] [connection signal="pressed" from="save_replay" to="." method="open_file_dialog"]

View File

@ -102,7 +102,7 @@ offset_bottom = 100.0
grow_horizontal = 2 grow_horizontal = 2
size_flags_vertical = 3 size_flags_vertical = 3
[node name="h_box_container" type="PlayerListings" parent="scoreboard" unique_id=43264288] [node name="player_listings" type="PlayerListings" parent="scoreboard" unique_id=43264288]
player_listing_scene = ExtResource("2_8qguk") player_listing_scene = ExtResource("2_8qguk")
layout_mode = 2 layout_mode = 2
alignment = 1 alignment = 1

View File

@ -4,8 +4,9 @@
[ext_resource type="Texture2D" uid="uid://7u2jx45bwejo" path="res://raw_assets/ping/Ping_medium.png" id="2_h6por"] [ext_resource type="Texture2D" uid="uid://7u2jx45bwejo" path="res://raw_assets/ping/Ping_medium.png" id="2_h6por"]
[ext_resource type="Texture2D" uid="uid://c0n5ugd867e2k" path="res://raw_assets/ping/Ping_high.png" id="3_kx1on"] [ext_resource type="Texture2D" uid="uid://c0n5ugd867e2k" path="res://raw_assets/ping/Ping_high.png" id="3_kx1on"]
[ext_resource type="Theme" uid="uid://drydbnrcdv4pk" path="res://default_theme.tres" id="4_kx1on"] [ext_resource type="Theme" uid="uid://drydbnrcdv4pk" path="res://default_theme.tres" id="4_kx1on"]
[ext_resource type="Texture2D" uid="uid://dv850alocb47k" path="res://raw_assets/icons/PNG/White/1x/menuList.png" id="5_gadtc"]
[node name="player_listing" type="PlayerListing" unique_id=965142972 node_paths=PackedStringArray("name_label", "kills_label", "deaths_label", "ping_label", "ping_icon")] [node name="player_listing" type="PlayerListing" unique_id=965142972 node_paths=PackedStringArray("name_label", "kills_label", "deaths_label", "ping_label", "ping_icon", "stats_button")]
name_label = NodePath("name") name_label = NodePath("name")
kills_label = NodePath("kills") kills_label = NodePath("kills")
deaths_label = NodePath("deaths") deaths_label = NodePath("deaths")
@ -17,6 +18,7 @@ ping_medium_color = Color(0.7963379, 0.79633814, 0, 1)
ping_high = ExtResource("3_kx1on") ping_high = ExtResource("3_kx1on")
ping_high_color = Color(0.99999994, 0.22953579, 0.17418489, 1) ping_high_color = Color(0.99999994, 0.22953579, 0.17418489, 1)
ping_icon = NodePath("ping_icon") ping_icon = NodePath("ping_icon")
stats_button = NodePath("button")
size_flags_horizontal = 3 size_flags_horizontal = 3
theme = ExtResource("4_kx1on") theme = ExtResource("4_kx1on")
@ -53,3 +55,9 @@ layout_mode = 2
texture = ExtResource("3_kx1on") texture = ExtResource("3_kx1on")
expand_mode = 2 expand_mode = 2
stretch_mode = 4 stretch_mode = 4
[node name="button" type="Button" parent="." unique_id=1272088409]
custom_minimum_size = Vector2(20, 0)
layout_mode = 2
icon = ExtResource("5_gadtc")
expand_icon = true

View File

@ -0,0 +1,35 @@
[gd_scene format=3 uid="uid://thi85gv3blli"]
[node name="player_stats_panel" type="PlayerStatsPanel" unique_id=820294448 node_paths=PackedStringArray("stats_grid")]
stats_grid = NodePath("v_box_container/grid_container")
visible = false
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -6.0
offset_top = -6.0
offset_right = 6.0
offset_bottom = 6.0
grow_horizontal = 2
grow_vertical = 2
[node name="v_box_container" type="VBoxContainer" parent="." unique_id=1545420438]
layout_mode = 2
[node name="label" type="Label" parent="v_box_container" unique_id=1011731554]
layout_mode = 2
text = "PlayerName"
horizontal_alignment = 1
[node name="grid_container" type="GridContainer" parent="v_box_container" unique_id=1825400706]
layout_mode = 2
columns = 2
[node name="button" type="Button" parent="v_box_container" unique_id=210952362]
layout_mode = 2
size_flags_horizontal = 4
text = "Close"
[connection signal="pressed" from="v_box_container/button" to="." method="close"]

View File

@ -421,8 +421,7 @@ impl Game {
peer.broadcast_reliable(Package::SetReady(player.id(), true)); peer.broadcast_reliable(Package::SetReady(player.id(), true));
} }
for player in &mut self.players { for player in &mut self.players {
player.data.kills = 0; player.stats = Default::default();
player.data.deaths = 0;
} }
self.goals.clear(); self.goals.clear();
} }
@ -569,9 +568,8 @@ impl Game {
ping: 0, ping: 0,
team: 0, team: 0,
ready: false, ready: false,
kills: 0,
deaths: 0,
}, },
stats: Default::default(),
killing_spree: 0, killing_spree: 0,
killing_spree_cd: 0., killing_spree_cd: 0.,
character: None, character: None,
@ -740,6 +738,13 @@ impl Game {
.insert(*team, self.goals.get(&team).copied().unwrap_or(0) + 1); .insert(*team, self.goals.get(&team).copied().unwrap_or(0) + 1);
} }
if let Some(scorer_player) = self.find_player_mut(scorer) {
scorer_player.stats.goals += 1;
if teams.contains(&scorer_player.data.team) {
scorer_player.stats.own_goals += 1;
}
}
if let Some(self_player) = self.get_self() if let Some(self_player) = self.get_self()
&& let Some(mut local_player) = self_player.get_local_player() && let Some(mut local_player) = self_player.get_local_player()
{ {
@ -1079,7 +1084,7 @@ impl Game {
.record_event(ReplayEvent::Death(source, player_id)); .record_event(ReplayEvent::Death(source, player_id));
} }
let total_prev_kills: u16 = self.players.iter().map(|p| p.data.kills).sum(); let total_prev_kills: u16 = self.players.iter().map(|p| p.stats.kills).sum();
if let Some(chat) = &mut Chat::singleton() { if let Some(chat) = &mut Chat::singleton() {
chat.bind_mut() chat.bind_mut()
@ -1089,9 +1094,10 @@ impl Game {
let killing_spree = if let DamageSource::Player(source_id) = source let killing_spree = if let DamageSource::Player(source_id) = source
&& let Some(source_player) = self.find_player_mut(source_id) && let Some(source_player) = self.find_player_mut(source_id)
{ {
source_player.data.kills += 1; source_player.stats.kills += 1;
source_player.killing_spree += 1; source_player.killing_spree += 1;
source_player.killing_spree_cd = 10.; source_player.killing_spree_cd = 10.;
source_player.killing_spree source_player.killing_spree
} else { } else {
0 0
@ -1100,8 +1106,29 @@ impl Game {
if let Some(player) = self.find_player_mut(player_id) { if let Some(player) = self.find_player_mut(player_id) {
player.killing_spree = 0; player.killing_spree = 0;
player.killing_spree_cd = 0.; player.killing_spree_cd = 0.;
player.data.deaths += 1; player.stats.deaths += 1;
} }
// Record friendly-fire kills
if let Some(target) = self.find_player(player_id).cloned()
&& let DamageSource::Player(source_id) = source
&& let Some(source_player) = self.find_player_mut(source_id)
{
if target.data.team == source_player.data.team {
source_player.stats.friendly_kills += 1;
}
}
// Record friendly-fire deaths
if let DamageSource::Player(source_id) = source
&& let Some(source_player) = self.find_player(source_id).cloned()
&& let Some(target) = self.find_player_mut(player_id)
{
if target.data.team == source_player.data.team {
target.stats.friendly_deaths += 1;
}
}
self.run_deferred(move |s| { self.run_deferred(move |s| {
s.signals().on_player_killed().emit(player_id); s.signals().on_player_killed().emit(player_id);
@ -1354,6 +1381,7 @@ impl Game {
pub struct Player { pub struct Player {
pub connection_addr: SocketAddr, pub connection_addr: SocketAddr,
pub data: NetPlayer, pub data: NetPlayer,
pub stats: PlayerStats,
pub character: Option<DynGd<CharacterBody3D, dyn IPlayer + 'static>>, pub character: Option<DynGd<CharacterBody3D, dyn IPlayer + 'static>>,
pub killing_spree: u16, pub killing_spree: u16,
pub killing_spree_cd: f64, pub killing_spree_cd: f64,
@ -1495,6 +1523,16 @@ impl Player {
} }
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct PlayerStats {
pub kills: u16,
pub deaths: u16,
pub goals: u16,
pub own_goals: u16,
pub friendly_kills: u16,
pub friendly_deaths: u16,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NetPlayer { pub struct NetPlayer {
pub id: u16, pub id: u16,
@ -1502,8 +1540,6 @@ pub struct NetPlayer {
pub name: String, pub name: String,
pub ping: u128, pub ping: u128,
pub team: u8, pub team: u8,
pub kills: u16,
pub deaths: u16,
} }
impl From<NetPlayer> for Player { impl From<NetPlayer> for Player {
@ -1511,6 +1547,7 @@ impl From<NetPlayer> for Player {
Player { Player {
connection_addr: SocketAddr::from(([0, 0, 0, 0], 0)), connection_addr: SocketAddr::from(([0, 0, 0, 0], 0)),
data: value, data: value,
stats: Default::default(),
character: None, character: None,
killing_spree: 0, killing_spree: 0,
killing_spree_cd: 0., killing_spree_cd: 0.,

View File

@ -9,7 +9,7 @@ use godot::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
game::{Game, GameManager, Player}, game::{Game, GameManager, Player, PlayerStats},
game_settings::GameSettingsManager, game_settings::GameSettingsManager,
map::Map, map::Map,
net::{network_manager::BallSync, util::NetVector3}, net::{network_manager::BallSync, util::NetVector3},
@ -514,8 +514,7 @@ impl INode for ReplayRecorder {
name: player.data.name.clone(), name: player.data.name.clone(),
color: team.bind().color.into(), color: team.bind().color.into(),
team: player.data.team, team: player.data.team,
kills: 0, stats: Default::default(),
deaths: 0,
}, },
); );
} }
@ -585,8 +584,7 @@ impl ReplayRecorder {
player_data.insert(player.id(), state); player_data.insert(player.id(), state);
} }
if let Some(replay_player) = self.replay.players.get_mut(&player.id()) { if let Some(replay_player) = self.replay.players.get_mut(&player.id()) {
replay_player.kills = player.data.kills; replay_player.stats = player.stats.clone();
replay_player.deaths = player.data.deaths;
} }
} }
ball_data = if let Some(ball) = &game.bind().world_ball { ball_data = if let Some(ball) = &game.bind().world_ball {
@ -688,8 +686,7 @@ pub struct ReplayPlayer {
pub name: String, pub name: String,
pub color: ReplayColor, pub color: ReplayColor,
pub team: u8, pub team: u8,
pub kills: u16, pub stats: PlayerStats,
pub deaths: u16,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -778,8 +775,8 @@ impl Player {
.map(|w| w.dyn_bind().get_type()) .map(|w| w.dyn_bind().get_type())
.unwrap_or(WeaponType::Raygun), .unwrap_or(WeaponType::Raygun),
is_dead: character.dyn_bind().is_dead(), is_dead: character.dyn_bind().is_dead(),
kills: player.data.kills, kills: player.stats.kills,
deaths: player.data.deaths, deaths: player.stats.deaths,
health: character.dyn_bind().get_health(), health: character.dyn_bind().get_health(),
buffs: character.dyn_bind().get_buffs().clone(), buffs: character.dyn_bind().get_buffs().clone(),
}) })

View File

@ -9,6 +9,7 @@ pub mod main_menu;
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 player_stats_panel;
pub mod replay_finished_screen; pub mod replay_finished_screen;
pub mod replay_hud; pub mod replay_hud;
pub mod settings; pub mod settings;

View File

@ -1,12 +1,14 @@
use godot::{ use godot::{
classes::{HBoxContainer, IHBoxContainer, Label, Texture2D, TextureRect}, classes::{Button, HBoxContainer, IHBoxContainer, Label, Texture2D, TextureRect},
prelude::*, prelude::*,
}; };
use crate::{game::Game, replay::StandaloneReplayManager}; use crate::{
game::Game, replay::StandaloneReplayManager, ui::player_stats_panel::PlayerStatsPanel,
};
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=HBoxContainer, init)] #[class(base=HBoxContainer, init, tool)]
pub struct PlayerListing { pub struct PlayerListing {
#[export] #[export]
name_label: Option<Gd<Label>>, name_label: Option<Gd<Label>>,
@ -30,9 +32,14 @@ pub struct PlayerListing {
ping_high_color: Color, ping_high_color: Color,
#[export] #[export]
ping_icon: Option<Gd<TextureRect>>, ping_icon: Option<Gd<TextureRect>>,
#[export]
stats_button: Option<Gd<Button>>,
pub player_id: Option<u16>, pub player_id: Option<u16>,
#[export]
pub stats_panel: Option<Gd<PlayerStatsPanel>>,
base: Base<HBoxContainer>, base: Base<HBoxContainer>,
} }
@ -58,6 +65,20 @@ impl IHBoxContainer for PlayerListing {
.connect_other(s, |s| s.update()); .connect_other(s, |s| s.update());
} }
}); });
if let Some(mut stats_button) = self.stats_button.clone() {
stats_button.set_visible(self.stats_panel.is_some());
if let Some(player_id) = self.player_id {
stats_button
.signals()
.pressed()
.connect_other(self, move |s| {
if let Some(panel) = &mut s.stats_panel {
panel.bind_mut().show(player_id);
}
});
}
}
} }
} }
@ -70,10 +91,10 @@ impl PlayerListing {
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.stats.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.stats.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());

View File

@ -4,7 +4,11 @@ use godot::{
prelude::*, prelude::*,
}; };
use crate::{game::Game, replay::StandaloneReplayManager, ui::player_listing::PlayerListing}; use crate::{
game::Game,
replay::StandaloneReplayManager,
ui::{player_listing::PlayerListing, player_stats_panel::PlayerStatsPanel},
};
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=HBoxContainer, init)] #[class(base=HBoxContainer, init)]
@ -13,6 +17,8 @@ pub struct PlayerListings {
player_listing_scene: Option<Gd<PackedScene>>, player_listing_scene: Option<Gd<PackedScene>>,
#[export] #[export]
update_on_disconnect: bool, update_on_disconnect: bool,
#[export]
player_stats_panel: Option<Gd<PlayerStatsPanel>>,
base: Base<HBoxContainer>, base: Base<HBoxContainer>,
} }
@ -65,6 +71,7 @@ impl PlayerListings {
for player in players { for player in players {
let mut listing = listing_scene.instantiate_as::<PlayerListing>(); let mut listing = listing_scene.instantiate_as::<PlayerListing>();
listing.bind_mut().player_id = Some(player.id()); listing.bind_mut().player_id = Some(player.id());
listing.bind_mut().stats_panel = self.player_stats_panel.clone();
vbox.add_child(&listing); vbox.add_child(&listing);
} }
} }
@ -99,6 +106,7 @@ impl PlayerListings {
for (_, player) in players { for (_, player) in players {
let mut listing = listing_scene.instantiate_as::<PlayerListing>(); let mut listing = listing_scene.instantiate_as::<PlayerListing>();
listing.bind_mut().player_id = Some(player.id); listing.bind_mut().player_id = Some(player.id);
listing.bind_mut().stats_panel = self.player_stats_panel.clone();
vbox.add_child(&listing); vbox.add_child(&listing);
} }
} }

View File

@ -0,0 +1,77 @@
use godot::{
classes::{GridContainer, Label, PanelContainer},
prelude::*,
};
use crate::{
game::{Game, PlayerStats},
replay::StandaloneReplayManager,
};
#[derive(GodotClass)]
#[class(base=PanelContainer, init)]
pub struct PlayerStatsPanel {
#[export]
player_name: Option<Gd<Label>>,
#[export]
stats_grid: Option<Gd<GridContainer>>,
base: Base<PanelContainer>,
}
#[godot_api]
impl PlayerStatsPanel {
#[func]
pub fn close(&mut self) {
self.base_mut().set_visible(false);
}
pub fn show(&mut self, player_id: u16) {
let stats = if let Some(game) = Game::singleton()
&& let Some(player) = game.bind().find_player(player_id)
{
Some((player.data.name.clone(), player.stats.clone()))
} else if let Some(playback) = &StandaloneReplayManager::singleton().bind().playback
&& let Some(player) = playback.bind().replay.players.get(&player_id)
{
Some((player.name.clone(), player.stats.clone()))
} else {
None
};
if let Some(name_label) = &mut self.player_name
&& let Some((name, _)) = &stats
{
name_label.set_text(name);
}
if let Some(grid) = &mut self.stats_grid {
for child in grid.get_children().iter_shared() {
grid.remove_child(&child);
}
if let Some((_, stats)) = stats {
let stats = vec![
("Kills", stats.kills.to_string()),
("Deaths", stats.deaths.to_string()),
("Goals", stats.goals.to_string()),
("Own Goals", stats.own_goals.to_string()),
("Friendly Kills", stats.friendly_kills.to_string()),
("Friendly Deaths", stats.friendly_deaths.to_string()),
];
for (stat_name, stat) in stats {
let mut label = Label::new_alloc();
label.set_text(&stat_name.to_owned());
grid.add_child(&label);
let mut label = Label::new_alloc();
label.set_text(&stat.to_owned());
grid.add_child(&label);
}
}
}
self.base_mut().set_visible(true);
}
}