Compare commits
26 Commits
b8578a0a95
...
5b1f840e40
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b1f840e40 | |||
| 8093ffb2be | |||
| e75cb79c86 | |||
| 6cd8515449 | |||
| 460b362d5c | |||
| 8c0d5b99ff | |||
| 5687adbf8a | |||
| b3ecbfccd8 | |||
| 506d0f3b50 | |||
| 934b06657b | |||
| d79411b7e7 | |||
| 8abcd9ec68 | |||
| c0e746a8e2 | |||
| 721e98f63c | |||
| b33ba26b5e | |||
| a304691d1b | |||
| e9343f9f48 | |||
| 6c533b522d | |||
| 8552aeb55d | |||
| 6badb845b5 | |||
| 91f76b3836 | |||
| f67843cc59 | |||
| 5748a0af7b | |||
| 5c51e25a11 | |||
| 9b522f0789 | |||
| 7a9292242f |
3
.gitignore
vendored
3
.gitignore
vendored
@ -11,4 +11,5 @@ godot/maps/entity_models
|
||||
rust/target
|
||||
target
|
||||
|
||||
dist
|
||||
dist
|
||||
*.replay
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -200,6 +200,7 @@ dependencies = [
|
||||
name = "quakeball"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ciborium",
|
||||
"godot",
|
||||
"serde",
|
||||
"teanet",
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://ct70bpbca8one" path="res://scenes/player/local_player.tscn" id="1_yjvui"]
|
||||
[ext_resource type="PackedScene" uid="uid://kfxc0migw80o" path="res://scenes/player/remote_player.tscn" id="2_kcncr"]
|
||||
[ext_resource type="PackedScene" uid="uid://hmh2lrrlo2wy" path="res://scenes/player/replay_player.tscn" id="3_8af6u"]
|
||||
[ext_resource type="PackedScene" uid="uid://dfnbc8sbufa4j" path="res://scenes/misc/ball.tscn" id="3_e0nj3"]
|
||||
[ext_resource type="PackedScene" uid="uid://c1r8nk58cci3a" path="res://scenes/misc/goal.tscn" id="4_w7til"]
|
||||
[ext_resource type="PackedScene" uid="uid://dc17w5n6y1an" path="res://scenes/misc/replay_ball.tscn" id="5_oqkce"]
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_3vyb7"]
|
||||
|
||||
@ -24,7 +26,9 @@ spawners = [NodePath("team1_spawner"), NodePath("team2_spawner")]
|
||||
ball_spawner = NodePath("ball_spawner")
|
||||
local_player = ExtResource("1_yjvui")
|
||||
remote_player = ExtResource("2_kcncr")
|
||||
replay_player = ExtResource("3_8af6u")
|
||||
ball = ExtResource("3_e0nj3")
|
||||
replay_ball = ExtResource("5_oqkce")
|
||||
|
||||
[node name="terrain" type="MeshInstance3D" parent="." unique_id=1474127252]
|
||||
material_override = SubResource("StandardMaterial3D_u8vuu")
|
||||
|
||||
@ -23,6 +23,7 @@ GameManagerGlobal="*uid://bii6018k3ip3t"
|
||||
CliParserGlobal="*uid://c52o7xa0fnr53"
|
||||
GameSettingsGlobal="*uid://bjr15hjiojecw"
|
||||
AnnouncerGlobal="*uid://5gey4i80mmsa"
|
||||
StandaloneReplayGlobal="*uid://djmb2jqcpkuep"
|
||||
|
||||
[display]
|
||||
|
||||
|
||||
@ -2,9 +2,11 @@
|
||||
|
||||
[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")]
|
||||
[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")
|
||||
back_button = NodePath("button")
|
||||
player_listings = NodePath("player_listings")
|
||||
save_file_dialog = NodePath("save_dialog")
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
@ -39,6 +41,22 @@ grow_vertical = 0
|
||||
theme_override_font_sizes/font_size = 32
|
||||
text = "Back"
|
||||
|
||||
[node name="save_replay" type="Button" parent="." unique_id=1199584793]
|
||||
layout_mode = 1
|
||||
anchors_preset = 7
|
||||
anchor_left = 0.5
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -534.0
|
||||
offset_top = -87.0
|
||||
offset_right = -342.0
|
||||
offset_bottom = -34.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
theme_override_font_sizes/font_size = 32
|
||||
text = "Save Replay"
|
||||
|
||||
[node name="player_listings" type="PlayerListings" parent="." unique_id=602626392]
|
||||
player_listing_scene = ExtResource("1_ou354")
|
||||
layout_mode = 1
|
||||
@ -62,3 +80,8 @@ offset_top = 93.0
|
||||
offset_right = 20.0
|
||||
offset_bottom = 133.0
|
||||
grow_horizontal = 2
|
||||
|
||||
[node name="save_dialog" type="FileDialog" parent="." unique_id=1637714468]
|
||||
filters = PackedStringArray("*.replay")
|
||||
|
||||
[connection signal="pressed" from="save_replay" to="." method="open_file_dialog"]
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://dsxu0y8pjbw3k" path="res://scenes/ui/credits.tscn" id="1_sugp2"]
|
||||
[ext_resource type="PackedScene" uid="uid://n3u0jr4wiib1" path="res://scenes/ui/settings.tscn" id="2_0wfyh"]
|
||||
|
||||
[node name="menu" type="MainMenu" unique_id=2036763876 node_paths=PackedStringArray("camera", "main_position", "host_position", "main_panel", "host_panel", "join_panel", "host_port", "join_addr", "recent_servers_button", "popup_panel", "popup_title", "popup_text", "popup_button")]
|
||||
[node name="menu" type="MainMenu" unique_id=2036763876 node_paths=PackedStringArray("camera", "main_position", "host_position", "main_panel", "host_panel", "join_panel", "host_port", "join_addr", "recent_servers_button", "popup_panel", "popup_title", "popup_text", "popup_button", "open_replay_dialog")]
|
||||
camera = NodePath("camera_3d")
|
||||
main_position = NodePath("main_pos")
|
||||
host_position = NodePath("host_pos")
|
||||
@ -19,6 +19,7 @@ popup_title = NodePath("popup_panel/v_box_container/title")
|
||||
popup_text = NodePath("popup_panel/v_box_container/text")
|
||||
popup_button = NodePath("popup_panel/v_box_container/ok")
|
||||
credits = ExtResource("1_sugp2")
|
||||
open_replay_dialog = NodePath("open_replay_dialog")
|
||||
|
||||
[node name="camera_3d" type="Camera3D" parent="." unique_id=1513321931]
|
||||
transform = Transform3D(0.98629165, -0.031186381, 0.16203775, -1.4054481e-08, 0.981978, 0.1889952, -0.16501158, -0.18640438, 0.9685167, 1.0974052, 1.7166122, 1.4909106)
|
||||
@ -74,6 +75,11 @@ layout_mode = 2
|
||||
text = "Join Game"
|
||||
metadata/_edit_lock_ = true
|
||||
|
||||
[node name="play_replay" type="Button" parent="main_panel/v_box_container" unique_id=275552647]
|
||||
layout_mode = 2
|
||||
text = "Play Replay"
|
||||
metadata/_edit_lock_ = true
|
||||
|
||||
[node name="credits" type="Button" parent="main_panel/v_box_container" unique_id=953921422]
|
||||
layout_mode = 2
|
||||
text = "Credits"
|
||||
@ -245,8 +251,14 @@ autowrap_mode = 3
|
||||
layout_mode = 2
|
||||
text = "Ok"
|
||||
|
||||
[node name="open_replay_dialog" type="FileDialog" parent="." unique_id=925465558]
|
||||
title = "Open a File"
|
||||
file_mode = 0
|
||||
filters = PackedStringArray("*.replay")
|
||||
|
||||
[connection signal="pressed" from="main_panel/v_box_container/host" to="." method="go_to_host_game"]
|
||||
[connection signal="pressed" from="main_panel/v_box_container/join" to="." method="go_to_join_game"]
|
||||
[connection signal="pressed" from="main_panel/v_box_container/play_replay" to="." method="open_replay_dialog"]
|
||||
[connection signal="pressed" from="main_panel/v_box_container/credits" to="." method="open_credits"]
|
||||
[connection signal="pressed" from="main_panel/v_box_container/settings" to="settings" method="open"]
|
||||
[connection signal="pressed" from="main_panel/v_box_container/quit" to="." method="quit_game"]
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_1cgct"]
|
||||
|
||||
[node name="ball" type="Ball" unique_id=816748600]
|
||||
collision_layer = 12
|
||||
collision_layer = 8
|
||||
collision_mask = 7
|
||||
contact_monitor = true
|
||||
max_contacts_reported = 10
|
||||
|
||||
@ -5,11 +5,21 @@
|
||||
[ext_resource type="PackedScene" uid="uid://df84sctteh4kw" path="res://scenes/game_finished.tscn" id="1_qf2qp"]
|
||||
[ext_resource type="MapResource" uid="uid://sc3507f75002" path="res://maps/diner.tres" id="2_glms7"]
|
||||
[ext_resource type="PackedScene" uid="uid://lahirbqfppvw" path="res://scenes/main.tscn" id="3_7ele1"]
|
||||
[ext_resource type="PackedScene" uid="uid://ct70bpbca8one" path="res://scenes/player/local_player.tscn" id="4_3b0rc"]
|
||||
[ext_resource type="PackedScene" uid="uid://kfxc0migw80o" path="res://scenes/player/remote_player.tscn" id="5_nxoss"]
|
||||
[ext_resource type="MapResource" uid="uid://bsjoandgu476y" path="res://maps/two_towers.tres" id="5_rotn8"]
|
||||
[ext_resource type="PackedScene" uid="uid://hmh2lrrlo2wy" path="res://scenes/player/replay_player.tscn" id="6_5fjlq"]
|
||||
[ext_resource type="PackedScene" uid="uid://dfnbc8sbufa4j" path="res://scenes/misc/ball.tscn" id="7_cv0ql"]
|
||||
[ext_resource type="PackedScene" uid="uid://dc17w5n6y1an" path="res://scenes/misc/replay_ball.tscn" id="8_m1inn"]
|
||||
|
||||
[node name="game_manager" type="GameManager" unique_id=1442044769]
|
||||
respawn_timer = 10.0
|
||||
lobby_screen = ExtResource("1_3ama4")
|
||||
game_finished_screen = ExtResource("1_qf2qp")
|
||||
main_menu = ExtResource("3_7ele1")
|
||||
local_player = ExtResource("4_3b0rc")
|
||||
remote_player = ExtResource("5_nxoss")
|
||||
replay_player = ExtResource("6_5fjlq")
|
||||
ball = ExtResource("7_cv0ql")
|
||||
replay_ball = ExtResource("8_m1inn")
|
||||
maps = Array[MapResource]([ExtResource("1_glms7"), ExtResource("2_glms7"), ExtResource("5_rotn8")])
|
||||
|
||||
16
godot/scenes/misc/replay_ball.tscn
Normal file
16
godot/scenes/misc/replay_ball.tscn
Normal file
@ -0,0 +1,16 @@
|
||||
[gd_scene format=3 uid="uid://dc17w5n6y1an"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://r13poamopmnv" path="res://mesh/ball_mesh.tscn" id="1_ishne"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_1cgct"]
|
||||
|
||||
[node name="replay_ball" type="Ball" unique_id=816748600]
|
||||
collision_layer = 8
|
||||
collision_mask = 4
|
||||
contact_monitor = true
|
||||
max_contacts_reported = 10
|
||||
|
||||
[node name="mesh_instance_3d" parent="." unique_id=37904686 instance=ExtResource("1_ishne")]
|
||||
|
||||
[node name="collision_shape_3d" type="CollisionShape3D" parent="." unique_id=1408432914]
|
||||
shape = SubResource("SphereShape3D_1cgct")
|
||||
6
godot/scenes/misc/standalone_replay_global.tscn
Normal file
6
godot/scenes/misc/standalone_replay_global.tscn
Normal file
@ -0,0 +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")
|
||||
98
godot/scenes/player/replay_player.tscn
Normal file
98
godot/scenes/player/replay_player.tscn
Normal file
@ -0,0 +1,98 @@
|
||||
[gd_scene format=3 uid="uid://hmh2lrrlo2wy"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bod6b3befmyt5" path="res://scenes/player/soldier_m.tscn" id="1_bfcd3"]
|
||||
[ext_resource type="PackedScene" uid="uid://cl66unms2gim0" path="res://scenes/player/effects.tscn" id="2_rejks"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1cgct"]
|
||||
|
||||
[sub_resource type="ViewportTexture" id="ViewportTexture_1cgct"]
|
||||
viewport_path = NodePath("health_bar_sprite/health_bar_viewport")
|
||||
|
||||
[sub_resource type="Curve" id="Curve_yli5f"]
|
||||
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0]
|
||||
point_count = 2
|
||||
|
||||
[sub_resource type="CurveTexture" id="CurveTexture_3g557"]
|
||||
width = 70
|
||||
curve = SubResource("Curve_yli5f")
|
||||
|
||||
[sub_resource type="Curve" id="Curve_dmbod"]
|
||||
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0]
|
||||
point_count = 2
|
||||
|
||||
[sub_resource type="CurveTexture" id="CurveTexture_1cgct"]
|
||||
width = 70
|
||||
curve = SubResource("Curve_dmbod")
|
||||
|
||||
[sub_resource type="Curve" id="Curve_qn18y"]
|
||||
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0]
|
||||
point_count = 2
|
||||
|
||||
[sub_resource type="CurveTexture" id="CurveTexture_840kl"]
|
||||
width = 70
|
||||
curve = SubResource("Curve_qn18y")
|
||||
|
||||
[node name="player" type="RemotePlayer" unique_id=845968872 node_paths=PackedStringArray("soldier_mesh", "collider", "effects", "spectator_camera", "camera_origin", "name_label", "health_bar", "health_bar_sprite")]
|
||||
move_speed = 5.0
|
||||
soldier_mesh = NodePath("soldier_m")
|
||||
collider = NodePath("collision_shape_3d")
|
||||
effects = NodePath("effects")
|
||||
health = 100
|
||||
punch_cooldown = 1.0
|
||||
spectator_camera = NodePath("spectate_root/spectate_camera")
|
||||
camera_origin = NodePath("camera_origin")
|
||||
name_label = NodePath("name_label")
|
||||
health_bar = NodePath("health_bar_sprite/health_bar_viewport/health_bar")
|
||||
health_bar_sprite = NodePath("health_bar_sprite")
|
||||
collision_layer = 16
|
||||
collision_mask = 4
|
||||
|
||||
[node name="soldier_m" parent="." unique_id=281995630 instance=ExtResource("1_bfcd3")]
|
||||
transform = Transform3D(-0.5, 0, -4.371139e-08, 0, 0.5, 0, 4.371139e-08, 0, -0.5, 0, 0, 0)
|
||||
|
||||
[node name="collision_shape_3d" type="CollisionShape3D" parent="." unique_id=1110995513]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
|
||||
shape = SubResource("CapsuleShape3D_1cgct")
|
||||
|
||||
[node name="effects" parent="." unique_id=986677517 instance=ExtResource("2_rejks")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4540546, 0)
|
||||
|
||||
[node name="spectate_root" type="Node3D" parent="." unique_id=624336254]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7611488, 0)
|
||||
|
||||
[node name="spectate_camera" type="Camera3D" parent="spectate_root" unique_id=1408688016]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.89379096, 0.44848388, 0, -0.44848388, 0.89379096, 0, 0.6601318, 1.0407685)
|
||||
|
||||
[node name="camera_origin" type="Node3D" parent="." unique_id=1445680156]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8724979, -0.2644562)
|
||||
|
||||
[node name="name_label" type="Label3D" parent="." unique_id=142179925]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.3304863, 0)
|
||||
pixel_size = 0.002
|
||||
billboard = 1
|
||||
text = "asd"
|
||||
font_size = 64
|
||||
|
||||
[node name="health_bar_sprite" type="Sprite3D" parent="." unique_id=1478315510]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.1606798, 0)
|
||||
billboard = 1
|
||||
texture = SubResource("ViewportTexture_1cgct")
|
||||
|
||||
[node name="health_bar_viewport" type="SubViewport" parent="health_bar_sprite" unique_id=1819065918]
|
||||
disable_3d = true
|
||||
transparent_bg = true
|
||||
size = Vector2i(70, 10)
|
||||
size_2d_override = Vector2i(70, 10)
|
||||
size_2d_override_stretch = true
|
||||
|
||||
[node name="health_bar" type="TextureProgressBar" parent="health_bar_sprite/health_bar_viewport" unique_id=121411216]
|
||||
offset_right = 70.0
|
||||
offset_bottom = 1.0
|
||||
scale = Vector2(1, 5)
|
||||
value = 100.0
|
||||
texture_under = SubResource("CurveTexture_3g557")
|
||||
texture_over = SubResource("CurveTexture_1cgct")
|
||||
texture_progress = SubResource("CurveTexture_840kl")
|
||||
tint_under = Color(0, 0, 0, 0.3647059)
|
||||
tint_over = Color(1, 1, 1, 0)
|
||||
tint_progress = Color(0.5660922, 1.7688051e-06, 3.3691526e-07, 1)
|
||||
65
godot/scenes/replay_finished.tscn
Normal file
65
godot/scenes/replay_finished.tscn
Normal 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
|
||||
@ -7,6 +7,7 @@ edition = "2024"
|
||||
crate-type = ["cdylib"] # Compile this crate to a dynamic C library.
|
||||
|
||||
[dependencies]
|
||||
ciborium = "0.2.2"
|
||||
godot = "0.5.4"
|
||||
serde = "1.0.228"
|
||||
teanet = { path = "../teanet-rust" }
|
||||
|
||||
@ -23,6 +23,7 @@ use crate::{
|
||||
DamageSource, IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer,
|
||||
remote_player::RemotePlayer, weapon::WeaponType,
|
||||
},
|
||||
replay::{ReplayEvent, ReplayPlayback, ReplayRecorder},
|
||||
team_resource::TeamResource,
|
||||
ui::cli::{Command, CommandLinePanel},
|
||||
};
|
||||
@ -41,6 +42,22 @@ pub struct GameManager {
|
||||
#[export]
|
||||
pub main_menu: Option<Gd<PackedScene>>,
|
||||
|
||||
#[export]
|
||||
#[export_group(name = "Prefabs")]
|
||||
pub local_player: Option<Gd<PackedScene>>,
|
||||
#[export]
|
||||
#[export_group(name = "Prefabs")]
|
||||
pub remote_player: Option<Gd<PackedScene>>,
|
||||
#[export]
|
||||
#[export_group(name = "Prefabs")]
|
||||
pub replay_player: Option<Gd<PackedScene>>,
|
||||
#[export]
|
||||
#[export_group(name = "Prefabs")]
|
||||
pub ball: Option<Gd<PackedScene>>,
|
||||
#[export]
|
||||
#[export_group(name = "Prefabs")]
|
||||
pub replay_ball: Option<Gd<PackedScene>>,
|
||||
|
||||
pub game: Option<Gd<Game>>,
|
||||
base: Base<Node>,
|
||||
|
||||
@ -229,6 +246,9 @@ pub struct Game {
|
||||
|
||||
pub goals: HashMap<u8, u16>,
|
||||
|
||||
pub replay_recorder: Option<Gd<ReplayRecorder>>,
|
||||
pub current_replay_playback: Option<Gd<ReplayPlayback>>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
@ -312,6 +332,12 @@ impl INode for Game {
|
||||
player.killing_spree = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(playback) = &mut self.current_replay_playback
|
||||
&& let Some(recorder) = &self.replay_recorder
|
||||
{
|
||||
playback.bind_mut().replay = recorder.bind().replay.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,9 +363,49 @@ impl Game {
|
||||
pub fn on_player_killed(player: u16);
|
||||
|
||||
pub fn singleton() -> Option<Gd<Game>> {
|
||||
GameManager::singleton()
|
||||
.get_child(0)
|
||||
.map(|g| g.cast::<Game>())
|
||||
if GameManager::singleton().get_children().len() > 0 {
|
||||
GameManager::singleton()
|
||||
.get_child(0)
|
||||
.map(|g| g.cast::<Game>())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_playback(&mut self, time_delta: f64, spectated_id: u16) {
|
||||
if let Some(recorder) = &self.replay_recorder {
|
||||
let mut playback = ReplayPlayback::new_alloc();
|
||||
playback.bind_mut().replay = recorder.bind().replay.clone();
|
||||
playback.bind_mut().current_map = self.current_map.clone();
|
||||
playback.bind_mut().current_time = (recorder.bind().time_elapsed + time_delta).max(0.);
|
||||
playback.bind_mut().fast_forwarding = true;
|
||||
playback.bind_mut().spectated_player_id = spectated_id;
|
||||
self.current_replay_playback = Some(playback.clone());
|
||||
self.base_mut().add_child(&playback);
|
||||
}
|
||||
|
||||
for player in &mut self.players {
|
||||
if let Some(character) = &mut player.character {
|
||||
character.set_visible(false);
|
||||
}
|
||||
}
|
||||
if let Some(ball) = &mut self.world_ball {
|
||||
ball.set_visible(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_playback(&mut self) {
|
||||
if let Some(mut playback) = self.current_replay_playback.take() {
|
||||
playback.queue_free();
|
||||
}
|
||||
for player in &mut self.players {
|
||||
if let Some(character) = &mut player.character {
|
||||
character.set_visible(true);
|
||||
}
|
||||
}
|
||||
if let Some(ball) = &mut self.world_ball {
|
||||
ball.set_visible(true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_game(&mut self) {
|
||||
@ -362,9 +428,17 @@ impl Game {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let recorder = ReplayRecorder::new_alloc();
|
||||
self.replay_recorder = Some(recorder.clone());
|
||||
self.base_mut().add_child(&recorder);
|
||||
}
|
||||
|
||||
pub fn reset_game(&mut self) {
|
||||
if let Some(mut recorder) = self.replay_recorder.take() {
|
||||
recorder.queue_free();
|
||||
}
|
||||
|
||||
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))
|
||||
@ -380,6 +454,10 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn finish_game(&mut self) {
|
||||
if let Some(recorder) = &mut self.replay_recorder {
|
||||
recorder.bind_mut().stop_recording();
|
||||
}
|
||||
|
||||
self.game_started = false;
|
||||
|
||||
for player in &mut self.players {
|
||||
@ -749,18 +827,30 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn spawn_player_server(&mut self, id: u16) -> Option<DynGd<CharacterBody3D, dyn IPlayer>> {
|
||||
if let Some(player) = self.find_player_mut(id)
|
||||
&& let Some(character) = &mut player.character
|
||||
if id == self.self_id.unwrap_or(0) {
|
||||
self.stop_playback();
|
||||
}
|
||||
|
||||
if let Some(player) = self.find_player(id)
|
||||
&& let Some(mut character) = player.character.clone()
|
||||
{
|
||||
// This is a respawn, remove old character
|
||||
character.queue_free();
|
||||
|
||||
if let Some(mut recorder) = self.replay_recorder.clone()
|
||||
&& let Some(state) = player.get_state()
|
||||
{
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Respawn(id, state));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut map) = self.current_map.clone() {
|
||||
let team = self.find_player(id).map(|p| p.data.team).unwrap_or(0);
|
||||
let mut character =
|
||||
map.bind_mut()
|
||||
.spawn_player(id, team, id == self.self_id.unwrap_or(0), None);
|
||||
.spawn_player(id, team, id == self.self_id.unwrap_or(0), None, true);
|
||||
|
||||
if let Some(character) = &mut character {
|
||||
if let Some(player) = self.find_player(id) {
|
||||
@ -818,11 +908,23 @@ impl Game {
|
||||
|
||||
// Method called on the client-side whenever a player has spawned
|
||||
pub fn spawn_player_client(&mut self, id: u16, transform: NetTransform) {
|
||||
if id == self.self_id.unwrap_or(0) {
|
||||
self.stop_playback();
|
||||
}
|
||||
|
||||
if let Some(map) = &mut self.current_map {
|
||||
if let Some(player) = self.players.iter_mut().find(|p| p.id() == id) {
|
||||
if let Some(character) = &mut player.character {
|
||||
// This is a respawn, delete old character
|
||||
character.queue_free();
|
||||
|
||||
if let Some(mut recorder) = self.replay_recorder.clone()
|
||||
&& let Some(state) = player.get_state()
|
||||
{
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Respawn(id, state));
|
||||
}
|
||||
}
|
||||
|
||||
let mut character = map.bind_mut().spawn_player(
|
||||
@ -830,6 +932,7 @@ impl Game {
|
||||
player.data.team,
|
||||
player.id() == self.self_id.unwrap_or(0),
|
||||
Some(transform),
|
||||
true,
|
||||
);
|
||||
if let Some(character) = &mut character {
|
||||
character
|
||||
@ -886,6 +989,11 @@ impl Game {
|
||||
// Handle a jump, either from the server or from a client
|
||||
pub fn handle_jump(&mut self, player_id: u16) {
|
||||
if player_id != self.self_id.unwrap_or(0) {
|
||||
if let Some(recorder) = &mut self.replay_recorder {
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Jump(player_id));
|
||||
}
|
||||
if let Some(player) = self.find_player_mut(player_id) {
|
||||
player.apply_jump();
|
||||
}
|
||||
@ -902,8 +1010,13 @@ impl Game {
|
||||
deal_damage: bool,
|
||||
) -> bool {
|
||||
let self_id = self.self_id.clone().unwrap_or(0);
|
||||
if let Some(player) = self.find_player_mut(player_id) {
|
||||
if player.id() != self_id {
|
||||
if player_id != self_id {
|
||||
if let Some(recorder) = &mut self.replay_recorder {
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Shoot(player_id, to.into()));
|
||||
}
|
||||
if let Some(player) = self.find_player_mut(player_id) {
|
||||
player.try_shoot(to, transform, look_up, deal_damage)
|
||||
} else {
|
||||
false
|
||||
@ -923,8 +1036,13 @@ impl Game {
|
||||
deal_damage: bool,
|
||||
) -> bool {
|
||||
let self_id = self.self_id.clone().unwrap_or(0);
|
||||
if let Some(player) = self.find_player_mut(player_id) {
|
||||
if player.id() != self_id {
|
||||
if player_id != self_id {
|
||||
if let Some(recorder) = &mut self.replay_recorder {
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Punch(player_id, to.into()));
|
||||
}
|
||||
if let Some(player) = self.find_player_mut(player_id) {
|
||||
player.try_punch(to, transform, look_up, deal_damage)
|
||||
} else {
|
||||
false
|
||||
@ -936,6 +1054,12 @@ impl Game {
|
||||
|
||||
// Handle for when a player is killed, run both on client and server
|
||||
pub fn on_kill(&mut self, source: DamageSource, player_id: u16) {
|
||||
if let Some(recorder) = &mut self.replay_recorder {
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Death(source, player_id));
|
||||
}
|
||||
|
||||
if let DamageSource::Player(source_player_id) = source
|
||||
&& let Some(player) = self.find_player(source_player_id)
|
||||
&& let Some(character) = player.character.clone()
|
||||
@ -1026,6 +1150,12 @@ impl Game {
|
||||
|
||||
// Handle taking damage, from server to client
|
||||
pub fn handle_take_damage(&mut self, source: DamageSource, player_id: u16, damage: i32) {
|
||||
if let Some(recorder) = &mut self.replay_recorder {
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::TakeDamage(player_id, source, damage));
|
||||
}
|
||||
|
||||
if let Some(player) = self.find_player_mut(player_id) {
|
||||
if let Some(character) = &mut player.character {
|
||||
character.dyn_bind_mut().take_damage(source, damage, false);
|
||||
@ -1045,6 +1175,14 @@ impl Game {
|
||||
|
||||
// Called whenever a player takes damage on the server-side
|
||||
pub fn on_take_damage(&mut self, source: DamageSource, target_player_id: u16, damage: i32) {
|
||||
if let Some(recorder) = &mut self.replay_recorder {
|
||||
recorder.bind_mut().record_event(ReplayEvent::TakeDamage(
|
||||
target_player_id,
|
||||
source,
|
||||
damage,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer {
|
||||
if let PeerKind::Server(peer, _) = peer {
|
||||
peer.broadcast_reliable(TakeDamage(source, target_player_id, damage));
|
||||
@ -1061,6 +1199,10 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recorder(&mut self) -> &mut Option<Gd<ReplayRecorder>> {
|
||||
&mut self.replay_recorder
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@ -13,6 +13,7 @@ pub mod map_resource;
|
||||
pub mod net;
|
||||
pub mod player;
|
||||
pub mod popup_queue;
|
||||
pub mod replay;
|
||||
pub mod team_resource;
|
||||
pub mod ui;
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
use godot::{classes::CharacterBody3D, prelude::*};
|
||||
|
||||
use crate::{
|
||||
game_manager::Game,
|
||||
game_manager::{Game, GameManager},
|
||||
net::util::NetVector3,
|
||||
player::{
|
||||
IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer,
|
||||
},
|
||||
replay::{ReplayPlayer, ReplayPlayerState, StandaloneReplayManager},
|
||||
};
|
||||
|
||||
#[derive(GodotClass)]
|
||||
@ -15,12 +16,6 @@ pub struct Map {
|
||||
pub spawners: Array<Gd<Node3D>>,
|
||||
#[export]
|
||||
pub ball_spawner: Option<Gd<Node3D>>,
|
||||
#[export]
|
||||
pub local_player: Option<Gd<PackedScene>>,
|
||||
#[export]
|
||||
pub remote_player: Option<Gd<PackedScene>>,
|
||||
#[export]
|
||||
pub ball: Option<Gd<PackedScene>>,
|
||||
|
||||
base: Base<Node3D>,
|
||||
}
|
||||
@ -30,6 +25,10 @@ impl INode3D for Map {
|
||||
fn ready(&mut self) {
|
||||
if let Some(game) = &mut Game::singleton() {
|
||||
game.bind_mut().on_map_loaded(self.to_gd());
|
||||
} else {
|
||||
StandaloneReplayManager::singleton()
|
||||
.bind_mut()
|
||||
.on_map_loaded(self.to_gd());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,9 +44,10 @@ impl Map {
|
||||
team: u8,
|
||||
is_local: bool,
|
||||
transform: Option<NetTransform>,
|
||||
get_remote_data: bool,
|
||||
) -> Option<DynGd<CharacterBody3D, dyn IPlayer + 'static>> {
|
||||
if is_local {
|
||||
if let Some(local_player) = &self.local_player {
|
||||
if let Some(local_player) = &GameManager::singleton().bind().local_player {
|
||||
let mut node = local_player.instantiate_as::<LocalPlayer>();
|
||||
node.bind_mut().player_id = Some(player_id);
|
||||
self.base_mut().add_child(&node);
|
||||
@ -69,9 +69,22 @@ impl Map {
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Some(remote_player) = &self.remote_player {
|
||||
} else if let Some(remote_player) = &GameManager::singleton().bind().remote_player {
|
||||
let mut node = remote_player.instantiate_as::<RemotePlayer>();
|
||||
node.bind_mut().player_id = Some(player_id);
|
||||
if get_remote_data {
|
||||
let mut node_clone = node.clone();
|
||||
self.run_deferred(move |_| {
|
||||
if let Some(game) = &mut Game::singleton()
|
||||
&& let Some(player) = game.bind().find_player(player_id)
|
||||
&& let Some(team) = game.bind().get_team(player.data.team)
|
||||
{
|
||||
node_clone.bind_mut().player_name = player.data.name.clone();
|
||||
node_clone.bind_mut().player_color = team.bind().color.clone();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.base_mut().add_child(&node);
|
||||
if let Some(transform) = transform {
|
||||
node.bind_mut()
|
||||
@ -93,6 +106,34 @@ impl Map {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_replay_player(
|
||||
&mut self,
|
||||
player_id: u16,
|
||||
player: &ReplayPlayer,
|
||||
state: ReplayPlayerState,
|
||||
) -> Option<Gd<RemotePlayer>> {
|
||||
if let Some(replay_player) = &GameManager::singleton().bind().replay_player {
|
||||
let mut node: Gd<RemotePlayer> = replay_player.instantiate_as();
|
||||
node.bind_mut().player_id = Some(player_id);
|
||||
node.bind_mut().player_name = player.name.clone();
|
||||
node.bind_mut().player_color = player.color.clone().into();
|
||||
self.base_mut().add_child(&node);
|
||||
|
||||
node.bind_mut()
|
||||
.base_mut()
|
||||
.set_global_position(state.transform.location.into());
|
||||
node.bind_mut()
|
||||
.base_mut()
|
||||
.set_global_rotation(state.transform.rotation.into());
|
||||
node.bind_mut().set_movement_dir(state.movement_dir.into());
|
||||
node.bind_mut().apply_look_up(state.look_up);
|
||||
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_ball(
|
||||
&mut self,
|
||||
transform: Option<NetTransform>,
|
||||
@ -100,7 +141,34 @@ impl Map {
|
||||
player_id: Option<u16>,
|
||||
) -> Option<Gd<Ball>> {
|
||||
if let Some(ball_spawner) = self.ball_spawner.clone()
|
||||
&& let Some(ball_prefab) = &self.ball
|
||||
&& let Some(ball_prefab) = &GameManager::singleton().bind().ball
|
||||
{
|
||||
let mut ball = ball_prefab.instantiate_as::<Ball>();
|
||||
ball.bind_mut().dropper = player_id;
|
||||
self.base_mut().add_child(&ball);
|
||||
if let Some(transform) = transform {
|
||||
ball.set_global_position(transform.location.into());
|
||||
ball.set_global_rotation(transform.rotation.into());
|
||||
} else {
|
||||
ball.set_global_position(ball_spawner.get_global_position());
|
||||
}
|
||||
if let Some(velocity) = velocity {
|
||||
ball.set_linear_velocity(velocity.into());
|
||||
}
|
||||
Some(ball)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_replay_ball(
|
||||
&mut self,
|
||||
transform: Option<NetTransform>,
|
||||
velocity: Option<NetVector3>,
|
||||
player_id: Option<u16>,
|
||||
) -> Option<Gd<Ball>> {
|
||||
if let Some(ball_spawner) = self.ball_spawner.clone()
|
||||
&& let Some(ball_prefab) = &GameManager::singleton().bind().replay_ball
|
||||
{
|
||||
let mut ball = ball_prefab.instantiate_as::<Ball>();
|
||||
ball.bind_mut().dropper = player_id;
|
||||
|
||||
@ -29,6 +29,7 @@ use crate::{
|
||||
soldier_mesh::SoldierMesh,
|
||||
weapon::{BallWeapon, Raygun, Shotgun, Weapon, WeaponType},
|
||||
},
|
||||
replay::ReplayEvent,
|
||||
ui::{chatbox::ChatBox, cli::CommandLinePanel, settings::SettingsPanel},
|
||||
};
|
||||
|
||||
@ -142,6 +143,13 @@ impl IPlayer for LocalPlayer {
|
||||
|
||||
fn jump(&mut self) {
|
||||
if self.try_jump() {
|
||||
if let Some(game) = &mut Game::singleton()
|
||||
&& let Some(recorder) = game.bind_mut().recorder()
|
||||
{
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Jump(self.player_id.unwrap_or(0)));
|
||||
}
|
||||
if let Some(effects) = &mut self.effects {
|
||||
effects.bind_mut().jump();
|
||||
}
|
||||
@ -163,7 +171,18 @@ impl IPlayer for LocalPlayer {
|
||||
fn apply_look_up(&mut self, _look: f32) {}
|
||||
|
||||
fn try_shoot(&mut self, to: Vector3, deal_damage: bool) -> bool {
|
||||
self.try_shoot_common(to, deal_damage)
|
||||
if self.try_shoot_common(to, deal_damage) {
|
||||
if let Some(game) = &mut Game::singleton()
|
||||
&& let Some(recorder) = game.bind_mut().recorder()
|
||||
{
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Shoot(self.player_id.unwrap_or(0), to.into()));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn try_punch(&mut self, to: Vector3, deal_damage: bool) -> bool {
|
||||
@ -173,7 +192,18 @@ impl IPlayer for LocalPlayer {
|
||||
weapon.set_rotation(Vector3::UP);
|
||||
}
|
||||
self.punch_cd_remaining = self.punch_cooldown;
|
||||
self.try_punch_common(to, deal_damage)
|
||||
if self.try_punch_common(to, deal_damage) {
|
||||
if let Some(game) = &mut Game::singleton()
|
||||
&& let Some(recorder) = game.bind_mut().recorder()
|
||||
{
|
||||
recorder
|
||||
.bind_mut()
|
||||
.record_event(ReplayEvent::Punch(self.player_id.unwrap_or(0), to.into()));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@ -350,10 +380,10 @@ impl ICharacterBody3D for LocalPlayer {
|
||||
|
||||
if let Some(camera) = &self.camera {
|
||||
if camera.is_current() && self.dead_for > 3. {
|
||||
if let Some(game) = Game::singleton()
|
||||
if let Some(mut game) = Game::singleton()
|
||||
&& let Some(killer_id) = self.killer_id
|
||||
{
|
||||
let game = game.bind();
|
||||
let mut game = game.bind_mut();
|
||||
let remote_player = game.find_player(killer_id);
|
||||
if let Some(remote_player) = &remote_player
|
||||
&& let Some(remote_character) = remote_player.character.clone()
|
||||
@ -363,6 +393,8 @@ impl ICharacterBody3D for LocalPlayer {
|
||||
{
|
||||
camera.set_current(true);
|
||||
}
|
||||
|
||||
game.start_playback(-5., killer_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,8 @@ pub struct RemotePlayer {
|
||||
health_bar_sprite: Option<Gd<Sprite3D>>,
|
||||
|
||||
pub player_id: Option<u16>,
|
||||
pub player_name: String,
|
||||
pub player_color: Color,
|
||||
|
||||
movement_dir: Vector3,
|
||||
y_speed: f32,
|
||||
@ -227,14 +229,9 @@ impl IPlayer for RemotePlayer {
|
||||
impl ICharacterBody3D for RemotePlayer {
|
||||
fn ready(&mut self) {
|
||||
self.run_deferred(|s| {
|
||||
if let Some(player_id) = s.player_id
|
||||
&& let Some(game) = Game::singleton()
|
||||
&& let Some(player) = game.bind().find_player(player_id)
|
||||
&& let Some(team) = game.bind().get_team(player.data.team)
|
||||
&& let Some(name_label) = &mut s.name_label
|
||||
{
|
||||
name_label.set_text(&player.data.name);
|
||||
name_label.set_modulate(team.bind().color);
|
||||
if let Some(name_label) = &mut s.name_label {
|
||||
name_label.set_text(&s.player_name);
|
||||
name_label.set_modulate(s.player_color);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ use crate::{
|
||||
game_settings::{GameSettingsManager, VolumeSettings},
|
||||
net::util::{cast_ray, shotgun_ray},
|
||||
player::{DamageSource, shootable::Shootable},
|
||||
replay::StandaloneReplayManager,
|
||||
};
|
||||
|
||||
pub trait Weapon {
|
||||
@ -94,6 +95,8 @@ impl Weapon for Raygun {
|
||||
.map(|r| r.position)
|
||||
.unwrap_or(source.get_global_position() + dir * 100.);
|
||||
|
||||
godot_print!("from {} to {}", source.get_global_position(), beam_end);
|
||||
|
||||
if let Some(prefab) = &self.bullet_prefab {
|
||||
let mut bullet = prefab.instantiate_as::<Beam>();
|
||||
|
||||
@ -105,6 +108,11 @@ impl Weapon for Raygun {
|
||||
&& let Some(map) = &mut game.bind_mut().current_map
|
||||
{
|
||||
map.add_child(&bullet);
|
||||
} else if let Some(playback) =
|
||||
&mut StandaloneReplayManager::singleton().bind_mut().playback
|
||||
&& let Some(map) = &mut playback.bind_mut().current_map
|
||||
{
|
||||
map.add_child(&bullet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
530
rust/src/replay.rs
Normal file
530
rust/src/replay.rs
Normal file
@ -0,0 +1,530 @@
|
||||
use std::{collections::HashMap, io::Cursor};
|
||||
|
||||
use godot::{
|
||||
classes::{FileAccess, file_access::ModeFlags},
|
||||
prelude::*,
|
||||
tools::get_autoload_by_name,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
game_manager::{Game, GameManager, Player},
|
||||
map::Map,
|
||||
net::{network_manager::BallSync, util::NetVector3},
|
||||
player::{
|
||||
DamageSource, IPlayer, NetTransform, ball::Ball, remote_player::RemotePlayer,
|
||||
weapon::WeaponType,
|
||||
},
|
||||
team_resource::TeamResource,
|
||||
};
|
||||
|
||||
pub const STANDALONE_REPLAY_GLOBAL: &str = "StandaloneReplayGlobal";
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=Node, init)]
|
||||
pub struct StandaloneReplayManager {
|
||||
#[export]
|
||||
replay_finished_screen: Option<Gd<PackedScene>>,
|
||||
|
||||
loading_replay: bool,
|
||||
map: Option<Gd<Map>>,
|
||||
pub playback: Option<Gd<ReplayPlayback>>,
|
||||
replay: Option<Replay>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
impl StandaloneReplayManager {
|
||||
pub fn singleton() -> Gd<StandaloneReplayManager> {
|
||||
get_autoload_by_name::<StandaloneReplayManager>(STANDALONE_REPLAY_GLOBAL)
|
||||
}
|
||||
|
||||
pub fn open_replay(&mut self, path: &GString) {
|
||||
if let Some(mut file) = FileAccess::open(path, ModeFlags::READ) {
|
||||
let mut bytes = Vec::new();
|
||||
while !file.eof_reached() {
|
||||
bytes.push(file.get_8());
|
||||
}
|
||||
|
||||
let bytes = Cursor::new(&mut bytes);
|
||||
if let Ok(res) = ciborium::from_reader::<Replay, _>(bytes) {
|
||||
self.loading_replay = true;
|
||||
let map_idx = res.map_idx;
|
||||
self.replay = Some(res);
|
||||
if let Some(map) = GameManager::singleton().bind().maps.get(map_idx as usize) {
|
||||
if let Some(scene) = &map.bind().scene {
|
||||
self.base().get_tree().change_scene_to_packed(scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_map_loaded(&mut self, map: Gd<Map>) {
|
||||
if self.loading_replay {
|
||||
self.loading_replay = false;
|
||||
self.map = Some(map.clone());
|
||||
|
||||
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.;
|
||||
playback.bind_mut().fast_forwarding = false;
|
||||
playback.bind_mut().spectated_player_id = 0;
|
||||
self.playback = Some(playback.clone());
|
||||
self.base_mut().add_child(&playback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
#[class(base=Node, init)]
|
||||
pub struct ReplayPlayback {
|
||||
pub replay: Replay,
|
||||
pub current_map: Option<Gd<Map>>,
|
||||
current_frame_idx: usize,
|
||||
player_characters: HashMap<u16, Gd<RemotePlayer>>,
|
||||
ball: Option<Gd<Ball>>,
|
||||
|
||||
pub current_time: f64,
|
||||
|
||||
pub spectated_player_id: u16,
|
||||
|
||||
pub fast_forwarding: bool,
|
||||
|
||||
pub finished: bool,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl ReplayPlayback {
|
||||
#[signal]
|
||||
pub fn on_finished();
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl INode for ReplayPlayback {
|
||||
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) {
|
||||
if let Some(camera) = &mut character.bind_mut().spectator_camera {
|
||||
if !camera.is_current() {
|
||||
camera.make_current();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn physics_process(&mut self, delta: f64) {
|
||||
self.current_time += delta;
|
||||
|
||||
while let Some(frame) = self.replay.frames.get(self.current_frame_idx)
|
||||
&& frame.time <= self.current_time
|
||||
{
|
||||
if let ReplayBall::World(ball_state) = &frame.ball_data {
|
||||
if let Some(ball) = &mut self.ball {
|
||||
ball.bind_mut().apply_sync(BallSync {
|
||||
transform: ball_state.transform,
|
||||
velocity: ball_state.velocity,
|
||||
});
|
||||
} else if let Some(map) = &mut self.current_map {
|
||||
if let Some(ball) = map.bind_mut().spawn_replay_ball(
|
||||
ball_state.transform.into(),
|
||||
ball_state.velocity.into(),
|
||||
None,
|
||||
) {
|
||||
self.ball = Some(ball);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(mut ball) = self.ball.take() {
|
||||
ball.queue_free();
|
||||
}
|
||||
}
|
||||
|
||||
if !self.fast_forwarding {
|
||||
if let Some(map) = &mut self.current_map {
|
||||
for (id, data) in &frame.player_data {
|
||||
if !self.player_characters.contains_key(id)
|
||||
&& !data.is_dead
|
||||
&& let Some(player) = self.replay.players.get(id)
|
||||
{
|
||||
if let Some(player) =
|
||||
map.bind_mut()
|
||||
.spawn_replay_player(*id, player, data.clone())
|
||||
{
|
||||
self.player_characters.insert(*id, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for event in &frame.events {
|
||||
match event {
|
||||
ReplayEvent::Death(damage_source, player_id) => {
|
||||
if let Some(character) = self.player_characters.get_mut(player_id) {
|
||||
character.bind_mut().kill(*damage_source);
|
||||
}
|
||||
}
|
||||
ReplayEvent::Shoot(player_id, to) => {
|
||||
if let Some(character) = self.player_characters.get_mut(player_id) {
|
||||
character.bind_mut().try_shoot((*to).into(), false);
|
||||
}
|
||||
}
|
||||
ReplayEvent::Respawn(player_id, data) => {
|
||||
if let Some(mut character) = self.player_characters.remove(player_id) {
|
||||
character.queue_free();
|
||||
}
|
||||
|
||||
if let Some(map) = &mut self.current_map
|
||||
&& let Some(player) = self.replay.players.get(player_id)
|
||||
{
|
||||
if let Some(player) = map.bind_mut().spawn_replay_player(
|
||||
*player_id,
|
||||
player,
|
||||
data.clone(),
|
||||
) {
|
||||
self.player_characters.insert(*player_id, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
ReplayEvent::Jump(player_id) => {
|
||||
if let Some(character) = self.player_characters.get_mut(player_id) {
|
||||
character.bind_mut().jump();
|
||||
}
|
||||
}
|
||||
ReplayEvent::Punch(player_id, to) => {
|
||||
if let Some(character) = self.player_characters.get_mut(player_id) {
|
||||
character.bind_mut().try_punch((*to).into(), false);
|
||||
}
|
||||
}
|
||||
ReplayEvent::TakeDamage(player_id, damage_source, damage) => {
|
||||
if let Some(character) = self.player_characters.get_mut(player_id) {
|
||||
character
|
||||
.bind_mut()
|
||||
.take_damage(*damage_source, *damage, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (id, mut character) in self.player_characters.clone() {
|
||||
if let Some(state) = frame.player_data.get(&id) {
|
||||
character.bind_mut().apply_transform(state.transform);
|
||||
character.bind_mut().apply_look_up(state.look_up);
|
||||
character
|
||||
.bind_mut()
|
||||
.set_movement_dir(state.movement_dir.into());
|
||||
let weapon = character.bind().get_weapon().clone();
|
||||
if let Some(weapon) = weapon
|
||||
&& weapon.dyn_bind().get_type() != state.current_weapon
|
||||
{
|
||||
character
|
||||
.bind_mut()
|
||||
.swap_weapon(state.current_weapon, false);
|
||||
}
|
||||
} else {
|
||||
character.queue_free();
|
||||
self.player_characters.remove(&id);
|
||||
}
|
||||
}
|
||||
self.current_frame_idx += 1;
|
||||
}
|
||||
|
||||
self.fast_forwarding = false;
|
||||
|
||||
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);
|
||||
}
|
||||
if let Some(mut ball) = self.ball.take() {
|
||||
ball.queue_free();
|
||||
}
|
||||
self.signals().on_finished().emit();
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_tree(&mut self) {
|
||||
for (_, character) in &mut self.player_characters {
|
||||
character.queue_free();
|
||||
}
|
||||
self.player_characters.clear();
|
||||
if let Some(mut ball) = self.ball.take() {
|
||||
ball.queue_free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=Node, init)]
|
||||
pub struct ReplayRecorder {
|
||||
pub time_elapsed: f64,
|
||||
|
||||
pub replay: Replay,
|
||||
current_frame: ReplayFrame,
|
||||
|
||||
recording: bool,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl INode for ReplayRecorder {
|
||||
fn ready(&mut self) {
|
||||
self.recording = true;
|
||||
self.run_deferred(|s| {
|
||||
s.current_frame = s.record_frame();
|
||||
|
||||
if let Some(game) = &Game::singleton() {
|
||||
for player in &game.bind().players {
|
||||
if let Some(team) = game.bind().get_team(player.data.team) {
|
||||
s.replay.players.insert(
|
||||
player.id(),
|
||||
ReplayPlayer {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn physics_process(&mut self, delta: f64) {
|
||||
if !self.recording {
|
||||
return;
|
||||
}
|
||||
self.time_elapsed += delta;
|
||||
|
||||
if self.time_elapsed > (self.current_frame.time + (1. / 60.)) {
|
||||
self.replay.frames.push(self.current_frame.clone());
|
||||
self.current_frame = self.record_frame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplayRecorder {
|
||||
pub fn save_to(&self, path: &GString) {
|
||||
if let Some(mut file) = FileAccess::open(path, ModeFlags::WRITE) {
|
||||
let mut buf = Vec::new();
|
||||
let cursor = Cursor::new(&mut buf);
|
||||
ciborium::into_writer(&self.replay, cursor).ok();
|
||||
for byte in buf {
|
||||
file.store_8(byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_event(&mut self, event: ReplayEvent) {
|
||||
self.current_frame.events.push(event);
|
||||
}
|
||||
|
||||
pub fn stop_recording(&mut self) {
|
||||
self.replay.frames.push(self.current_frame.clone());
|
||||
self.recording = false;
|
||||
}
|
||||
|
||||
fn record_frame(&mut self) -> ReplayFrame {
|
||||
let mut player_data = HashMap::new();
|
||||
|
||||
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() {
|
||||
player_data.insert(player.id(), state);
|
||||
}
|
||||
}
|
||||
ball_data = if let Some(ball) = &game.bind().world_ball {
|
||||
ReplayBall::World(ReplayBallData {
|
||||
transform: ball.bind().get_transform(),
|
||||
velocity: ball.bind().get_velocity(),
|
||||
})
|
||||
} 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Replay {
|
||||
pub map_idx: u8,
|
||||
pub players: HashMap<u16, ReplayPlayer>,
|
||||
pub teams: Vec<ReplayTeam>,
|
||||
pub goals: HashMap<u8, u16>,
|
||||
frames: Vec<ReplayFrame>,
|
||||
}
|
||||
|
||||
impl Default for Replay {
|
||||
fn default() -> Self {
|
||||
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)]
|
||||
pub struct ReplayColor {
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
a: f32,
|
||||
}
|
||||
|
||||
impl From<Color> for ReplayColor {
|
||||
fn from(value: Color) -> Self {
|
||||
ReplayColor {
|
||||
r: value.r,
|
||||
g: value.g,
|
||||
b: value.b,
|
||||
a: value.a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReplayColor> for Color {
|
||||
fn from(value: ReplayColor) -> Self {
|
||||
Color {
|
||||
r: value.r,
|
||||
g: value.g,
|
||||
b: value.b,
|
||||
a: value.a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplayFrame {
|
||||
time: f64,
|
||||
player_data: HashMap<u16, ReplayPlayerState>,
|
||||
ball_data: ReplayBall,
|
||||
events: Vec<ReplayEvent>,
|
||||
goals: HashMap<u8, u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ReplayEvent {
|
||||
Death(DamageSource, u16),
|
||||
Shoot(u16, NetVector3),
|
||||
Respawn(u16, ReplayPlayerState),
|
||||
Jump(u16),
|
||||
Punch(u16, NetVector3),
|
||||
TakeDamage(u16, DamageSource, i32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub enum ReplayBall {
|
||||
World(ReplayBallData),
|
||||
#[default]
|
||||
Held,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplayPlayerState {
|
||||
pub transform: NetTransform,
|
||||
pub movement_dir: NetVector3,
|
||||
pub look_up: f32,
|
||||
pub current_weapon: WeaponType,
|
||||
pub is_dead: bool,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn get_state(&self) -> Option<ReplayPlayerState> {
|
||||
if let Some(character) = &self.character {
|
||||
Some(ReplayPlayerState {
|
||||
transform: character.dyn_bind().get_transform(),
|
||||
movement_dir: character.dyn_bind().get_movement_dir().into(),
|
||||
look_up: character.dyn_bind().get_look_up(),
|
||||
current_weapon: character
|
||||
.dyn_bind()
|
||||
.get_weapon()
|
||||
.map(|w| w.dyn_bind().get_type())
|
||||
.unwrap_or(WeaponType::Raygun),
|
||||
is_dead: character.dyn_bind().is_dead(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplayBallData {
|
||||
transform: NetTransform,
|
||||
velocity: NetVector3,
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
use godot::{
|
||||
classes::{Button, HBoxContainer, IPanel, Label, Panel},
|
||||
classes::{Button, FileDialog, HBoxContainer, IPanel, Label, Panel},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
@ -18,6 +18,9 @@ pub struct GameFinishedScreen {
|
||||
#[export]
|
||||
player_listings: Option<Gd<PlayerListings>>,
|
||||
|
||||
#[export]
|
||||
save_file_dialog: Option<Gd<FileDialog>>,
|
||||
|
||||
base: Base<Panel>,
|
||||
}
|
||||
|
||||
@ -36,10 +39,24 @@ impl IPanel for GameFinishedScreen {
|
||||
GameManager::singleton().bind().go_to_lobby();
|
||||
});
|
||||
}
|
||||
if let Some(file_dialog) = self.save_file_dialog.clone() {
|
||||
file_dialog
|
||||
.signals()
|
||||
.file_selected()
|
||||
.connect_other(self, Self::save_replay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GameFinishedScreen {
|
||||
#[func]
|
||||
pub fn open_file_dialog(&mut self) {
|
||||
if let Some(file_dialog) = &mut self.save_file_dialog {
|
||||
file_dialog.popup_file_dialog();
|
||||
}
|
||||
}
|
||||
|
||||
fn produce_title(&mut self) {
|
||||
if let Some(game) = Game::singleton() {
|
||||
let max_goals = game
|
||||
@ -84,4 +101,12 @@ impl GameFinishedScreen {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use godot::{
|
||||
classes::{Button, Camera3D, Label, LineEdit, OptionButton, Panel, SpinBox},
|
||||
classes::{
|
||||
Button, Camera3D, FileAccess, FileDialog, Label, LineEdit, OptionButton, Panel, SpinBox,
|
||||
file_access::ModeFlags,
|
||||
},
|
||||
prelude::*,
|
||||
register::property::SimpleVar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
game_manager::GameManager,
|
||||
game_settings::{GameSettingsManager, RecentServer},
|
||||
net::network_manager::NetworkManager,
|
||||
popup_queue::{Popup, PopupQueue},
|
||||
replay::{Replay, StandaloneReplayManager},
|
||||
ui::credits::CreditsMenu,
|
||||
};
|
||||
|
||||
@ -61,6 +68,9 @@ pub struct MainMenu {
|
||||
#[export]
|
||||
credits: Option<Gd<PackedScene>>,
|
||||
|
||||
#[export]
|
||||
open_replay_dialog: Option<Gd<FileDialog>>,
|
||||
|
||||
prev_panel: Option<Gd<Panel>>,
|
||||
|
||||
recent_servers: Vec<RecentServer>,
|
||||
@ -87,6 +97,12 @@ impl INode3D for MainMenu {
|
||||
.item_selected()
|
||||
.connect_other(self, |s, idx| s.on_recent_server_selected(idx));
|
||||
}
|
||||
if let Some(dialog) = &self.open_replay_dialog {
|
||||
dialog
|
||||
.signals()
|
||||
.file_selected()
|
||||
.connect_other(self, Self::on_open_replay);
|
||||
}
|
||||
self.hide_popup();
|
||||
}
|
||||
|
||||
@ -162,6 +178,21 @@ impl INode3D for MainMenu {
|
||||
|
||||
#[godot_api]
|
||||
impl MainMenu {
|
||||
#[func]
|
||||
pub fn open_replay_dialog(&mut self) {
|
||||
if let Some(dialog) = &mut self.open_replay_dialog {
|
||||
dialog.popup_file_dialog();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_open_replay(&mut self, path: GString) {
|
||||
self.run_deferred(move |_| {
|
||||
StandaloneReplayManager::singleton()
|
||||
.bind_mut()
|
||||
.open_replay(&path);
|
||||
});
|
||||
}
|
||||
|
||||
#[func]
|
||||
pub fn host_game(&mut self) {
|
||||
if let Some(host_port) = &self.host_port {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::<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);
|
||||
}
|
||||
}
|
||||
|
||||
96
rust/src/ui/replay_finished_screen.rs
Normal file
96
rust/src/ui/replay_finished_screen.rs
Normal file
@ -0,0 +1,96 @@
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user