Compare commits

...

7 Commits

13 changed files with 413 additions and 21 deletions

View File

@ -9,6 +9,7 @@
[ext_resource type="Material" uid="uid://f4ne0rn5g5wh" path="res://maps/materials/green_ceiling_light.tres" id="7_8hfm3"]
[ext_resource type="Material" uid="uid://1ine08aphdv7" path="res://maps/materials/orange_accent.tres" id="7_ap2uw"]
[ext_resource type="LightmapGIData" uid="uid://hqspy3qsl5jd" path="res://maps/diner/diner.lmbake" id="9_g8xwm"]
[ext_resource type="PackedScene" uid="uid://c1r8nk58cci3a" path="res://scenes/misc/goal.tscn" id="10_wffmg"]
[sub_resource type="ArrayMesh" id="ArrayMesh_o17ro"]
lightmap_size_hint = Vector2i(354, 354)
@ -540,8 +541,8 @@ indices = PackedInt32Array(0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10,
[sub_resource type="BoxShape3D" id="BoxShape3D_1cgct"]
size = Vector3(1000, 10, 1000)
[node name="diner" type="Map" unique_id=1971127396 node_paths=PackedStringArray("spawner", "ball_spawner")]
spawner = NodePath("spawner")
[node name="diner" type="Map" unique_id=1971127396 node_paths=PackedStringArray("spawners", "ball_spawner")]
spawners = [NodePath("spawner"), NodePath("spawner2")]
ball_spawner = NodePath("ball_spawner")
local_player = ExtResource("1_wffmg")
remote_player = ExtResource("2_msi5w")
@ -1057,6 +1058,9 @@ shadow_enabled = true
[node name="spawner" type="Node3D" parent="." unique_id=2117785064]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12.922944, 0.50108063, -0.19082117)
[node name="spawner2" type="Node3D" parent="." unique_id=1061070767]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14.774757, 0.50108063, -14.130155)
[node name="killbox" type="Killbox" parent="." unique_id=1750431583]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -18.08466, 0)
collision_layer = 7
@ -1068,3 +1072,10 @@ shape = SubResource("BoxShape3D_1cgct")
[node name="ball_spawner" type="Node3D" parent="." unique_id=1771283598]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.6695356, 3.831614, 0.6756325)
[node name="goal2" parent="." unique_id=177528504 instance=ExtResource("10_wffmg")]
team = 1
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13.790253, -2.3841858e-07, -13.729624)
[node name="goal" parent="." unique_id=1795145790 instance=ExtResource("10_wffmg")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13.09099, 0, -1.3568912)

View File

@ -0,0 +1,57 @@
[gd_scene format=3 uid="uid://df84sctteh4kw"]
[ext_resource type="PackedScene" uid="uid://2sfy0vq0fqt3" path="res://scenes/ui/player_listing.tscn" id="1_ou354"]
[node name="game_finished" type="GameFinishedScreen" unique_id=700430082 node_paths=PackedStringArray("title_container", "back_button", "players_hbox")]
title_container = NodePath("title_container")
back_button = NodePath("button")
players_hbox = NodePath("player_listings")
player_listing_scene = ExtResource("1_ou354")
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 = -22.0
offset_top = -103.0
offset_right = 23.0
offset_bottom = -69.0
grow_horizontal = 2
grow_vertical = 0
theme_override_font_sizes/font_size = 32
text = "Back"
[node name="player_listings" type="HBoxContainer" parent="." unique_id=588134370]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -186.0
offset_top = -193.0
offset_right = 186.0
offset_bottom = -153.0
grow_horizontal = 2
grow_vertical = 2
alignment = 1

View File

@ -1,8 +1,12 @@
[gd_scene format=3 uid="uid://bii6018k3ip3t"]
[ext_resource type="PackedScene" uid="uid://bwd6bj21x310s" path="res://scenes/lobby.tscn" id="1_3ama4"]
[ext_resource type="MapResource" uid="uid://c7n1tacc7a73t" path="res://maps/default_map.tres" id="1_glms7"]
[ext_resource type="PackedScene" uid="uid://df84sctteh4kw" path="res://scenes/game_finished.tscn" id="1_qf2qp"]
[ext_resource type="MapResource" uid="uid://bsjoandgu476y" path="res://maps/diner.tres" id="2_glms7"]
[node name="game_manager" type="GameManager" unique_id=1442044769]
respawn_timer = 10.0
lobby_screen = ExtResource("1_3ama4")
game_finished_screen = ExtResource("1_qf2qp")
maps = Array[MapResource]([ExtResource("1_glms7"), ExtResource("2_glms7")])

View File

@ -0,0 +1,35 @@
[gd_scene format=3 uid="uid://2sfy0vq0fqt3"]
[node name="player_listing" type="PlayerListing" unique_id=965142972 node_paths=PackedStringArray("name_label", "kills_label", "deaths_label", "ping_label")]
name_label = NodePath("name")
kills_label = NodePath("kills")
deaths_label = NodePath("deaths")
ping_label = NodePath("ping")
[node name="name" type="Label" parent="." unique_id=1361759955]
custom_minimum_size = Vector2(200, 0)
custom_maximum_size = Vector2(200, -1)
clip_contents = true
layout_mode = 2
text = "Player Name"
[node name="kills" type="Label" parent="." unique_id=690390687]
custom_minimum_size = Vector2(30, 0)
custom_maximum_size = Vector2(30, -1)
layout_mode = 2
text = "0"
horizontal_alignment = 1
[node name="deaths" type="Label" parent="." unique_id=1615203485]
custom_minimum_size = Vector2(30, 0)
custom_maximum_size = Vector2(30, -1)
layout_mode = 2
text = "0"
horizontal_alignment = 1
[node name="ping" type="Label" parent="." unique_id=1700548535]
custom_minimum_size = Vector2(100, 0)
custom_maximum_size = Vector2(100, -1)
layout_mode = 2
text = "0ms"
horizontal_alignment = 1

View File

@ -27,6 +27,10 @@ pub const GAME_MANAGER_GLOBAL: &str = "GameManagerGlobal";
pub struct GameManager {
#[export]
pub respawn_timer: f64,
#[export]
pub lobby_screen: Option<Gd<PackedScene>>,
#[export]
pub game_finished_screen: Option<Gd<PackedScene>>,
pub game: Option<Gd<Game>>,
base: Base<Node>,
@ -73,6 +77,9 @@ impl GameManager {
game.signals()
.on_map_changed()
.connect_other(s, |s, id| s.signals().on_map_changed().emit(id));
game.signals()
.on_game_finished()
.connect_other(s, |s| s.go_to_finish_screen());
}
});
@ -80,6 +87,18 @@ impl GameManager {
self.game = Some(game);
}
pub fn go_to_finish_screen(&self) {
if let Some(scene) = &self.game_finished_screen {
self.base().get_tree().change_scene_to_packed(scene);
}
}
pub fn go_to_lobby(&self) {
if let Some(lobby) = &self.lobby_screen {
self.base().get_tree().change_scene_to_packed(lobby);
}
}
}
pub struct GameOptions {
@ -92,6 +111,7 @@ impl Default for GameOptions {
opts.insert(GameOption::AllowAnyMap, GameOptionValue::Boolean(false));
opts.insert(GameOption::AllowAnyTeam, GameOptionValue::Boolean(false));
opts.insert(GameOption::FriendlyFire, GameOptionValue::Boolean(true));
opts.insert(GameOption::GoalsNeededToWin, GameOptionValue::Number(10.));
Self { values: opts }
}
}
@ -118,6 +138,8 @@ pub enum GameOption {
AllowAnyTeam,
/// Allow anyone to shoot anyone, not just other team's members
FriendlyFire,
/// The number of goals needed to win a match
GoalsNeededToWin,
}
impl ToString for GameOption {
@ -126,6 +148,7 @@ impl ToString for GameOption {
GameOption::AllowAnyMap => "Allow anyone to select the map",
GameOption::AllowAnyTeam => "Allow anyone to select any teams",
GameOption::FriendlyFire => "Friendly fire",
GameOption::GoalsNeededToWin => "Goals needed to win",
}
.to_owned()
}
@ -134,6 +157,7 @@ impl ToString for GameOption {
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum GameOptionValue {
Boolean(bool),
Number(f64),
}
#[derive(GodotClass)]
@ -254,6 +278,8 @@ impl Game {
pub fn options_changed();
#[signal]
pub fn on_goal();
#[signal]
pub fn on_game_finished();
pub fn singleton() -> Option<Gd<Game>> {
GameManager::singleton()
@ -261,6 +287,51 @@ impl Game {
.map(|g| g.cast::<Game>())
}
pub fn reset_game(&mut self) {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer
&& let PeerKind::Server(peer, _) = peer
&& let Some(player) = self.find_player_mut(self.self_id.unwrap_or(0))
{
player.data.ready = true;
peer.broadcast_reliable(Package::SetReady(player.id(), true));
}
self.goals.clear();
}
pub fn finish_game(&mut self) {
for player in &mut self.players {
if let Some(mut character) = player.character.take() {
character.queue_free();
}
player.data.ready = false;
}
self.run_deferred(|s| {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer
&& let PeerKind::Server(peer, _) = peer
{
for player in &s.players {
peer.broadcast_reliable(Package::SetReady(player.id(), false));
}
}
});
if let Some(mut ball) = self.world_ball.take() {
ball.queue_free();
}
self.current_map.take();
self.game_started = false;
self.run_deferred(|s| {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer
&& let PeerKind::Server(peer, _) = peer
{
peer.broadcast_reliable(Package::FinishGame);
}
s.signals().on_game_finished().emit()
});
}
pub fn change_option(&mut self, opt: GameOption, value: GameOptionValue) {
self.opts.values.insert(opt, value);
self.run_deferred(move |s| {
@ -377,8 +448,8 @@ impl Game {
name: name.unwrap_or(format!("Player {}", id)),
ping: 0,
team: 0,
ready: false,
},
ready: false,
character: None,
});
self.run_deferred(move |s| s.signals().on_new_player().emit(id));
@ -451,7 +522,7 @@ impl Game {
pub fn update_player_ready(&mut self, player_id: u16, ready: bool) {
if let Some(player) = self.players.iter_mut().find(|p| p.id() == player_id) {
player.ready = ready;
player.data.ready = ready;
}
self.run_deferred(|s| s.signals().on_players_updated().emit());
}
@ -531,6 +602,21 @@ impl Game {
peer.broadcast_reliable(Package::Goal(teams));
}
}
if NetworkManager::singleton().bind().is_host() {
let max_goals = s.goals.values().max().copied().unwrap_or(0);
let needed_goals = match s
.opts
.values
.get(&GameOption::GoalsNeededToWin)
.unwrap_or(&&GameOptionValue::Number(10.))
{
GameOptionValue::Boolean(_) => 10,
GameOptionValue::Number(value) => *value as u16,
};
if max_goals >= needed_goals {
s.finish_game();
}
}
});
}
@ -754,7 +840,6 @@ impl Game {
pub struct Player {
pub connection_addr: SocketAddr,
pub data: NetPlayer,
pub ready: bool,
pub character: Option<DynGd<CharacterBody3D, dyn IPlayer + 'static>>,
}
@ -857,6 +942,7 @@ impl Player {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NetPlayer {
pub id: u16,
pub ready: bool,
pub name: String,
pub ping: u128,
pub team: u8,
@ -867,7 +953,6 @@ impl From<NetPlayer> for Player {
Player {
connection_addr: SocketAddr::from(([0, 0, 0, 0], 0)),
data: value,
ready: false,
character: None,
}
}

View File

@ -19,9 +19,6 @@ use crate::{
#[derive(GodotClass)]
#[class(base=Node)]
pub struct NetworkManager {
#[export]
lobby_scene: Option<Gd<PackedScene>>,
pub peer: Option<PeerKind>,
last_hello: Instant,
@ -37,8 +34,6 @@ pub const NETWORK_SINGLETON_NAME: &str = "NetworkManagerGlob";
impl INode for NetworkManager {
fn init(base: Base<Node>) -> Self {
NetworkManager {
lobby_scene: None,
peer: None,
last_hello: Instant::now(),
@ -219,10 +214,8 @@ impl NetworkManager {
}
pub fn swap_to_lobby(&self, is_host: bool) {
if let Some(scene) = &self.lobby_scene {
self.base().get_tree().change_scene_to_packed(scene);
GameManager::singleton().bind_mut().init_game(is_host);
}
GameManager::singleton().bind_mut().init_game(is_host);
GameManager::singleton().bind().go_to_lobby();
}
pub fn update_nick(&mut self, nick: String) {
@ -276,6 +269,9 @@ pub enum Package {
DespawnBall,
SwapWeapon(u16, WeaponType),
Goal(Vec<u8>),
// End-of-Game packages
FinishGame,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -176,6 +176,11 @@ impl NetworkManager {
game.bind_mut().handle_goal(teams);
}
}
Package::FinishGame => {
if let Some(game) = &mut Game::singleton() {
game.bind_mut().finish_game();
}
}
_ => {}
},
},

View File

@ -528,6 +528,10 @@ impl ICharacterBody3D for LocalPlayer {
}
}
}
fn exit_tree(&mut self) {
self.set_lock(false);
}
}
#[godot_api]

View File

@ -0,0 +1,124 @@
use godot::{
classes::{
Button, HBoxContainer, IPanel, Label, Panel, VBoxContainer, box_container::AlignmentMode,
control::LayoutDirection,
},
global::{HorizontalAlignment, VerticalAlignment},
prelude::*,
};
use crate::{
game_manager::{Game, GameManager},
ui::player_listing::PlayerListing,
};
#[derive(GodotClass)]
#[class(base=Panel, init)]
pub struct GameFinishedScreen {
#[export]
title_container: Option<Gd<HBoxContainer>>,
#[export]
back_button: Option<Gd<Button>>,
#[export]
players_hbox: Option<Gd<HBoxContainer>>,
#[export]
player_listing_scene: Option<Gd<PackedScene>>,
base: Base<Panel>,
}
#[godot_api]
impl IPanel for GameFinishedScreen {
fn ready(&mut self) {
self.produce_title();
self.produce_player_listings();
if let Some(back) = &self.back_button {
back.signals().pressed().connect_other(self, |_| {
if let Some(mut game) = Game::singleton() {
game.bind_mut().reset_game();
}
GameManager::singleton().bind().go_to_lobby();
});
}
}
}
impl GameFinishedScreen {
fn produce_player_listings(&mut self) {
if let Some(game) = Game::singleton() {
for (id, team) in game.bind().get_teams().iter_shared().enumerate() {
let mut vbox = VBoxContainer::new_alloc();
vbox.set_alignment(AlignmentMode::CENTER);
let mut team_label = Label::new_alloc();
team_label.set_modulate(team.bind().color);
team_label.set_text(&team.bind().name);
team_label.set_horizontal_alignment(HorizontalAlignment::CENTER);
vbox.add_child(&team_label);
if let Some(listing_scene) = &self.player_listing_scene {
let game = game.bind();
let players = game
.players
.iter()
.filter(|p| p.data.team == id as u8)
.collect::<Vec<_>>();
for player in players {
let mut listing = listing_scene.instantiate_as::<PlayerListing>();
listing.bind_mut().player_id = Some(player.id());
vbox.add_child(&listing);
}
}
if let Some(hbox) = &mut self.players_hbox {
hbox.add_child(&vbox);
}
}
}
}
fn produce_title(&mut self) {
if let Some(game) = Game::singleton() {
let max_goals = game
.bind()
.goals
.iter()
.map(|(_, v)| v)
.max()
.copied()
.unwrap_or(0);
let winning_team_ids = game
.bind()
.goals
.iter()
.filter(|(_, goals)| **goals == max_goals)
.map(|(team, _)| *team)
.collect::<Vec<_>>();
let winning_teams = game
.bind()
.get_teams()
.iter_shared()
.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.bind().name);
label.set_modulate(team.bind().color);
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);
}
}
}
}

View File

@ -2,8 +2,8 @@ use std::collections::HashMap;
use godot::{
classes::{
Button, CheckBox, Control, GridContainer, IPanel, Label, OptionButton, Panel, TextEdit,
VBoxContainer,
Button, CheckBox, Control, GridContainer, IPanel, Label, OptionButton, Panel, SpinBox,
TextEdit, VBoxContainer,
},
prelude::*,
};
@ -162,6 +162,22 @@ impl IPanel for LobbyPanel {
checkbox.upcast::<Control>()
}
GameOptionValue::Number(value) => {
let mut spinbox = SpinBox::new_alloc();
spinbox.set_value(*value);
spinbox.set_editable(NetworkManager::singleton().bind().is_host());
let spinbox_clone = spinbox.clone();
let option = option.clone();
spinbox.signals().changed().connect_other(self, move |s| {
s.option_changed(
option,
GameOptionValue::Number(spinbox_clone.get_value()),
);
});
spinbox.upcast()
}
};
nodes.push(value_node);
}
@ -209,6 +225,11 @@ impl LobbyPanel {
checkbox.set_pressed(*value);
}
}
GameOptionValue::Number(value) => {
if let Ok(mut spinbox) = control.try_cast::<SpinBox>() {
spinbox.set_value(*value);
}
}
}
}
}
@ -221,7 +242,7 @@ impl LobbyPanel {
if let PeerKind::Server(_, _) = peer
&& let Some(button) = &mut self.ready_button
{
let all_ready = game.bind().players.iter().all(|p| p.ready);
let all_ready = game.bind().players.iter().all(|p| p.data.ready);
button.set_disabled(!all_ready);
}
}

View File

@ -93,7 +93,7 @@ impl LobbyPlayerListing {
}
if let Some(ready_label) = &mut self.ready_label {
if player.ready {
if player.data.ready {
ready_label.set_text("Ready");
} else {
ready_label.set_text("Not Ready");
@ -130,7 +130,6 @@ impl LobbyPlayerListing {
&& let Some(player_label) = &mut self.name_label
&& let Some(team) = game.bind().get_team(player.data.team)
{
godot_print!("Setting color");
player_label.add_theme_color_override("font_color", team.bind().color);
}
}
@ -146,7 +145,6 @@ impl LobbyPlayerListing {
&& let Some(player_label) = &mut self.name_label
&& let Some(team) = game.bind().get_team(player.data.team)
{
godot_print!("Setting color");
player_label.add_theme_color_override("font_color", team.bind().color);
}
}

View File

@ -1,5 +1,7 @@
pub mod cli;
pub mod game_finished_screen;
pub mod lobby;
pub mod lobby_player_listing;
pub mod net_stats;
pub mod player_listing;
pub mod simple_score_view;

View File

@ -0,0 +1,50 @@
use godot::{
classes::{HBoxContainer, IHBoxContainer, Label},
prelude::*,
};
use crate::game_manager::Game;
#[derive(GodotClass)]
#[class(base=HBoxContainer, init)]
pub struct PlayerListing {
#[export]
name_label: Option<Gd<Label>>,
#[export]
kills_label: Option<Gd<Label>>,
#[export]
deaths_label: Option<Gd<Label>>,
#[export]
ping_label: Option<Gd<Label>>,
pub player_id: Option<u16>,
base: Base<HBoxContainer>,
}
#[godot_api]
impl IHBoxContainer for PlayerListing {
fn ready(&mut self) {
self.update();
if let Some(game) = Game::singleton() {
game.signals()
.on_players_updated()
.connect_other(self, |s| s.update());
}
}
}
impl PlayerListing {
pub fn update(&mut self) {
if let Some(game) = Game::singleton()
&& let Some(player) = game.bind().find_player(self.player_id.unwrap_or(0))
{
if let Some(name_label) = &mut self.name_label {
name_label.set_text(&player.data.name);
}
if let Some(ping_label) = &mut self.ping_label {
ping_label.set_text(&format!("{}ms", player.data.ping));
}
}
}
}