Feat/10 adding multiplayer fundamentals #14
@@ -61,6 +61,22 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
|
|
||||||
if health <= 0:
|
if health <= 0:
|
||||||
_die(hit_position, source, dmg_taken)
|
_die(hit_position, source, dmg_taken)
|
||||||
|
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm and multiplayer.has_multiplayer_peer():
|
||||||
|
var killer_id = 0
|
||||||
|
var weapon_name = "Killed"
|
||||||
|
if source:
|
||||||
|
if source.has_method("get_multiplayer_authority"):
|
||||||
|
killer_id = source.get_multiplayer_authority()
|
||||||
|
elif "owner_player" in source and source.owner_player:
|
||||||
|
killer_id = int(str(source.owner_player.name))
|
||||||
|
elif source is CharacterBody3D:
|
||||||
|
killer_id = int(str(source.name))
|
||||||
|
|
||||||
|
if source.has_method("get_weapon_name"):
|
||||||
|
weapon_name = source.get_weapon_name()
|
||||||
|
nm.register_kill.rpc(-1, killer_id, weapon_name, "Target Dummy")
|
||||||
else:
|
else:
|
||||||
_wiggle()
|
_wiggle()
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,22 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
|
|
||||||
if health <= 0:
|
if health <= 0:
|
||||||
_die(hit_position, source, dmg_taken)
|
_die(hit_position, source, dmg_taken)
|
||||||
|
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm and multiplayer.has_multiplayer_peer():
|
||||||
|
var killer_id = 0
|
||||||
|
var weapon_name = "Killed"
|
||||||
|
if source:
|
||||||
|
if source.has_method("get_multiplayer_authority"):
|
||||||
|
killer_id = source.get_multiplayer_authority()
|
||||||
|
elif "owner_player" in source and source.owner_player:
|
||||||
|
killer_id = int(str(source.owner_player.name))
|
||||||
|
elif source is CharacterBody3D:
|
||||||
|
killer_id = int(str(source.name))
|
||||||
|
|
||||||
|
if source.has_method("get_weapon_name"):
|
||||||
|
weapon_name = source.get_weapon_name()
|
||||||
|
nm.register_kill.rpc(-1, killer_id, weapon_name, "Walking Dummy")
|
||||||
else:
|
else:
|
||||||
_wiggle()
|
_wiggle()
|
||||||
|
|
||||||
|
|||||||
+148
-1
@@ -4,11 +4,24 @@ signal player_connected(id: int)
|
|||||||
signal player_disconnected(id: int)
|
signal player_disconnected(id: int)
|
||||||
signal connection_failed
|
signal connection_failed
|
||||||
signal connection_succeeded
|
signal connection_succeeded
|
||||||
|
signal match_state_updated
|
||||||
|
signal stats_updated
|
||||||
|
signal killfeed_event(victim: String, killer: String, weapon: String)
|
||||||
|
|
||||||
const DEFAULT_PORT = 31415
|
const DEFAULT_PORT = 31415
|
||||||
|
|
||||||
var connected_players: Array[int] = []
|
var connected_players: Array[int] = []
|
||||||
|
|
||||||
|
# Dict of ID -> { "username": String, "kills": int, "deaths": int, "assists": int, "ping": int }
|
||||||
|
var player_stats: Dictionary = {}
|
||||||
|
|
||||||
|
var match_active: bool = false
|
||||||
|
var match_time_remaining: float = 0.0
|
||||||
|
var current_gamemode: String = "Deathmatch"
|
||||||
|
var current_scene_path: String = ""
|
||||||
|
|
||||||
|
var _ping_timer: Timer
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||||
@@ -16,6 +29,12 @@ func _ready() -> void:
|
|||||||
multiplayer.connection_failed.connect(_on_connection_failed)
|
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||||
|
|
||||||
|
_ping_timer = Timer.new()
|
||||||
|
_ping_timer.wait_time = 1.0
|
||||||
|
_ping_timer.autostart = false
|
||||||
|
_ping_timer.timeout.connect(_on_ping_timer)
|
||||||
|
add_child(_ping_timer)
|
||||||
|
|
||||||
func host_game(port: int = DEFAULT_PORT) -> Error:
|
func host_game(port: int = DEFAULT_PORT) -> Error:
|
||||||
var peer = ENetMultiplayerPeer.new()
|
var peer = ENetMultiplayerPeer.new()
|
||||||
var err = peer.create_server(port)
|
var err = peer.create_server(port)
|
||||||
@@ -23,6 +42,8 @@ func host_game(port: int = DEFAULT_PORT) -> Error:
|
|||||||
multiplayer.multiplayer_peer = peer
|
multiplayer.multiplayer_peer = peer
|
||||||
connected_players.clear()
|
connected_players.clear()
|
||||||
connected_players.append(1) # Host is 1
|
connected_players.append(1) # Host is 1
|
||||||
|
_init_player_stats(1, SettingsManager.username)
|
||||||
|
_ping_timer.start()
|
||||||
print("Hosting on port %s" % port)
|
print("Hosting on port %s" % port)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
@@ -37,21 +58,56 @@ func join_game(ip: String, port: int = DEFAULT_PORT) -> Error:
|
|||||||
func disconnect_game() -> void:
|
func disconnect_game() -> void:
|
||||||
multiplayer.multiplayer_peer = null
|
multiplayer.multiplayer_peer = null
|
||||||
connected_players.clear()
|
connected_players.clear()
|
||||||
|
player_stats.clear()
|
||||||
|
match_active = false
|
||||||
|
_ping_timer.stop()
|
||||||
|
|
||||||
|
func start_singleplayer_match(gamemode: String = "Deathmatch") -> void:
|
||||||
|
# Ensure peer is offline if not already connected
|
||||||
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||||
|
|
||||||
|
connected_players.clear()
|
||||||
|
connected_players.append(1)
|
||||||
|
_init_player_stats(1, SettingsManager.username)
|
||||||
|
|
||||||
|
match_active = true
|
||||||
|
match_time_remaining = 300.0
|
||||||
|
current_gamemode = gamemode
|
||||||
|
rpc("sync_match_state", match_active, match_time_remaining, current_gamemode)
|
||||||
|
|
||||||
|
func _init_player_stats(id: int, username: String) -> void:
|
||||||
|
player_stats[id] = {
|
||||||
|
"username": username,
|
||||||
|
"kills": 0,
|
||||||
|
"deaths": 0,
|
||||||
|
"assists": 0,
|
||||||
|
"ping": 0
|
||||||
|
}
|
||||||
|
stats_updated.emit()
|
||||||
|
|
||||||
func _on_peer_connected(id: int) -> void:
|
func _on_peer_connected(id: int) -> void:
|
||||||
print("Player connected: ", id)
|
print("Player connected: ", id)
|
||||||
if not connected_players.has(id):
|
if not connected_players.has(id):
|
||||||
connected_players.append(id)
|
connected_players.append(id)
|
||||||
player_connected.emit(id)
|
|
||||||
|
# If server, we wait for the client to register themselves via RPC
|
||||||
|
pass
|
||||||
|
|
||||||
func _on_peer_disconnected(id: int) -> void:
|
func _on_peer_disconnected(id: int) -> void:
|
||||||
print("Player disconnected: ", id)
|
print("Player disconnected: ", id)
|
||||||
connected_players.erase(id)
|
connected_players.erase(id)
|
||||||
|
if player_stats.has(id):
|
||||||
|
player_stats.erase(id)
|
||||||
player_disconnected.emit(id)
|
player_disconnected.emit(id)
|
||||||
|
if multiplayer.is_server():
|
||||||
|
rpc("sync_player_stats", player_stats)
|
||||||
|
|
||||||
func _on_connected_to_server() -> void:
|
func _on_connected_to_server() -> void:
|
||||||
print("Successfully connected to server")
|
print("Successfully connected to server")
|
||||||
connection_succeeded.emit()
|
connection_succeeded.emit()
|
||||||
|
# Tell the server our username
|
||||||
|
rpc_id(1, "register_player", SettingsManager.username)
|
||||||
|
|
||||||
func _on_connection_failed() -> void:
|
func _on_connection_failed() -> void:
|
||||||
print("Failed to connect to server")
|
print("Failed to connect to server")
|
||||||
@@ -62,8 +118,99 @@ func _on_server_disconnected() -> void:
|
|||||||
print("Server disconnected")
|
print("Server disconnected")
|
||||||
multiplayer.multiplayer_peer = null
|
multiplayer.multiplayer_peer = null
|
||||||
connected_players.clear()
|
connected_players.clear()
|
||||||
|
player_stats.clear()
|
||||||
|
match_active = false
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable")
|
||||||
|
func register_player(username: String) -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var sender_id = multiplayer.get_remote_sender_id()
|
||||||
|
_init_player_stats(sender_id, username)
|
||||||
|
player_connected.emit(sender_id)
|
||||||
|
|
||||||
|
# Sync everyone's stats
|
||||||
|
rpc("sync_player_stats", player_stats)
|
||||||
|
|
||||||
|
# If a match is active, force the late-joiner to load the level
|
||||||
|
if match_active:
|
||||||
|
rpc_id(sender_id, "rpc_load_level", current_scene_path)
|
||||||
|
rpc_id(sender_id, "sync_match_state", match_active, match_time_remaining, current_gamemode)
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func sync_player_stats(stats: Dictionary) -> void:
|
||||||
|
player_stats = stats
|
||||||
|
stats_updated.emit()
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func sync_match_state(active: bool, time_rem: float, mode: String) -> void:
|
||||||
|
match_active = active
|
||||||
|
match_time_remaining = time_rem
|
||||||
|
current_gamemode = mode
|
||||||
|
match_state_updated.emit()
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func rpc_load_level(scene_path: String) -> void:
|
func rpc_load_level(scene_path: String) -> void:
|
||||||
print("Loading level: ", scene_path)
|
print("Loading level: ", scene_path)
|
||||||
|
current_scene_path = scene_path
|
||||||
|
if multiplayer.is_server():
|
||||||
|
match_active = true
|
||||||
|
match_time_remaining = 300.0 # 5 minutes
|
||||||
|
rpc("sync_match_state", match_active, match_time_remaining, current_gamemode)
|
||||||
get_tree().change_scene_to_file(scene_path)
|
get_tree().change_scene_to_file(scene_path)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if match_active:
|
||||||
|
if match_time_remaining > 0:
|
||||||
|
match_time_remaining -= delta
|
||||||
|
if match_time_remaining <= 0:
|
||||||
|
match_time_remaining = 0
|
||||||
|
match_active = false
|
||||||
|
# We emit this so HUDs can update their timers smoothly
|
||||||
|
match_state_updated.emit()
|
||||||
|
|
||||||
|
# --- PING SYSTEM ---
|
||||||
|
func _on_ping_timer() -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var time = Time.get_ticks_msec()
|
||||||
|
for pid in connected_players:
|
||||||
|
if pid != 1:
|
||||||
|
rpc_id(pid, "receive_ping_request", time)
|
||||||
|
rpc("sync_player_stats", player_stats)
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "unreliable")
|
||||||
|
func receive_ping_request(server_time: int) -> void:
|
||||||
|
rpc_id(1, "receive_ping_response", server_time)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "unreliable")
|
||||||
|
func receive_ping_response(server_time: int) -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var sender_id = multiplayer.get_remote_sender_id()
|
||||||
|
var rtt = Time.get_ticks_msec() - server_time
|
||||||
|
if player_stats.has(sender_id):
|
||||||
|
player_stats[sender_id]["ping"] = rtt
|
||||||
|
|
||||||
|
# --- GAMEPLAY EVENTS ---
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func register_kill(victim_id: int, killer_id: int, weapon_name: String, custom_victim_name: String = "") -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
if player_stats.has(victim_id):
|
||||||
|
player_stats[victim_id]["deaths"] += 1
|
||||||
|
if player_stats.has(killer_id) and killer_id != victim_id:
|
||||||
|
player_stats[killer_id]["kills"] += 1
|
||||||
|
|
||||||
|
var v_name = player_stats[victim_id]["username"] if player_stats.has(victim_id) else (custom_victim_name if custom_victim_name != "" else "Unknown")
|
||||||
|
var k_name = player_stats[killer_id]["username"] if player_stats.has(killer_id) else "Unknown"
|
||||||
|
|
||||||
|
# Auto-fallback for singleplayer testing where player_stats might not be initialized
|
||||||
|
if not player_stats.has(killer_id) and killer_id == 1:
|
||||||
|
k_name = SettingsManager.username
|
||||||
|
|
||||||
|
if killer_id == 0 or killer_id == victim_id:
|
||||||
|
k_name = "" # Suicide or world death
|
||||||
|
|
||||||
|
broadcast_killfeed.rpc(v_name, k_name, weapon_name)
|
||||||
|
sync_player_stats.rpc(player_stats)
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func broadcast_killfeed(victim: String, killer: String, weapon: String) -> void:
|
||||||
|
killfeed_event.emit(victim, killer, weapon)
|
||||||
|
|||||||
@@ -243,6 +243,37 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
|||||||
health = 0.0
|
health = 0.0
|
||||||
die(impulse)
|
die(impulse)
|
||||||
|
|
||||||
|
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer():
|
||||||
|
var victim_id = multiplayer.get_unique_id() if is_multiplayer_authority() else int(str(name))
|
||||||
|
var killer_id = 0
|
||||||
|
var weapon_name = "Killed"
|
||||||
|
if _source and _source is Node:
|
||||||
|
if _source.has_method("get_multiplayer_authority"):
|
||||||
|
killer_id = _source.get_multiplayer_authority()
|
||||||
|
elif "owner_player" in _source and _source.owner_player:
|
||||||
|
killer_id = int(str(_source.owner_player.name))
|
||||||
|
elif _source is PlayerMovementController:
|
||||||
|
killer_id = int(str(_source.name))
|
||||||
|
|
||||||
|
if _source.has_method("get_weapon_name"):
|
||||||
|
weapon_name = _source.get_weapon_name()
|
||||||
|
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "")
|
||||||
|
|
||||||
|
func get_weapon_name() -> String:
|
||||||
|
if camera:
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman:
|
||||||
|
var slot = wman.get("active_slot")
|
||||||
|
if slot != null and wman.weapons.has(slot):
|
||||||
|
var w = wman.weapons[slot]
|
||||||
|
if w and w.has_method("get_weapon_name"):
|
||||||
|
return w.get_weapon_name()
|
||||||
|
elif w:
|
||||||
|
# Format "res://weapons/assault_rifle.tscn" -> "Assault Rifle"
|
||||||
|
var p = w.scene_file_path.get_file().get_basename()
|
||||||
|
return p.capitalize()
|
||||||
|
return "Killed"
|
||||||
|
|
||||||
func _ensure_machine() -> MovementStateMachine:
|
func _ensure_machine() -> MovementStateMachine:
|
||||||
if is_instance_valid(_machine):
|
if is_instance_valid(_machine):
|
||||||
return _machine
|
return _machine
|
||||||
@@ -381,6 +412,13 @@ func _setup_hud() -> void:
|
|||||||
var vbox = VBoxContainer.new()
|
var vbox = VBoxContainer.new()
|
||||||
margin.add_child(vbox)
|
margin.add_child(vbox)
|
||||||
|
|
||||||
|
# Add Match HUD overlay
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
var match_hud_scene = load("res://ui/match_hud.tscn")
|
||||||
|
if match_hud_scene:
|
||||||
|
var match_hud = match_hud_scene.instantiate()
|
||||||
|
add_child(match_hud)
|
||||||
|
|
||||||
# Shield Bar (Top)
|
# Shield Bar (Top)
|
||||||
var shield_box = VBoxContainer.new()
|
var shield_box = VBoxContainer.new()
|
||||||
var s_title = Label.new()
|
var s_title = Label.new()
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ grapple={
|
|||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
scoreboard={
|
||||||
|
"deadzone": 0.0,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
var nm = load("res://globals/network_manager.gd").new()
|
||||||
|
nm.name = "NetworkManager"
|
||||||
|
root.add_child(nm)
|
||||||
|
|
||||||
|
var hud = load("res://ui/match_hud.gd").new()
|
||||||
|
hud.name = "MatchHUD"
|
||||||
|
root.add_child(hud)
|
||||||
|
|
||||||
|
# Mock network manager state
|
||||||
|
nm.match_active = true
|
||||||
|
nm._init_player_stats(1, "LocalTester")
|
||||||
|
|
||||||
|
# Try to call register_kill
|
||||||
|
nm.register_kill.rpc(-1, 1, "Rifle", "Target Dummy")
|
||||||
|
|
||||||
|
# Process one frame
|
||||||
|
call_deferred("quit")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://clrd47khl65oq
|
||||||
+43
-12
@@ -217,11 +217,25 @@ func _ready() -> void:
|
|||||||
hl_left.add_child(hl_title)
|
hl_left.add_child(hl_title)
|
||||||
|
|
||||||
var hl_cards = HBoxContainer.new()
|
var hl_cards = HBoxContainer.new()
|
||||||
hl_cards.add_theme_constant_override("separation", 30)
|
hl_cards.add_theme_constant_override("separation", 20)
|
||||||
hl_left.add_child(hl_cards)
|
hl_left.add_child(hl_cards)
|
||||||
|
|
||||||
_build_level_card(hl_cards, "Dust 2", "res://scenes/dust2_level/dust2_level.tscn", d2_gradient, true)
|
_build_level_card(hl_cards, "Dust 2", "res://scenes/dust2_level/dust2_level.tscn", d2_gradient, true, Vector2(200, 250))
|
||||||
_build_level_card(hl_cards, "Test Level", "res://scenes/test_level/test_level.tscn", tl_gradient, true)
|
_build_level_card(hl_cards, "Test Level", "res://scenes/test_level/test_level.tscn", tl_gradient, true, Vector2(200, 250))
|
||||||
|
|
||||||
|
var mode_label = Label.new()
|
||||||
|
mode_label.text = "Gamemode"
|
||||||
|
mode_label.add_theme_font_size_override("font_size", 32)
|
||||||
|
hl_left.add_child(mode_label)
|
||||||
|
|
||||||
|
var _gamemode_opt = OptionButton.new()
|
||||||
|
_gamemode_opt.add_theme_font_size_override("font_size", 24)
|
||||||
|
_gamemode_opt.add_item("Deathmatch", 0)
|
||||||
|
_gamemode_opt.item_selected.connect(func(index: int):
|
||||||
|
if get_node_or_null("/root/NetworkManager"):
|
||||||
|
get_node("/root/NetworkManager").current_gamemode = _gamemode_opt.get_item_text(index)
|
||||||
|
)
|
||||||
|
hl_left.add_child(_gamemode_opt)
|
||||||
|
|
||||||
var hl_spacer = Control.new()
|
var hl_spacer = Control.new()
|
||||||
hl_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
hl_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
@@ -251,21 +265,21 @@ func _ready() -> void:
|
|||||||
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
|
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
|
||||||
hl_right.add_child(_host_lobby_players_list)
|
hl_right.add_child(_host_lobby_players_list)
|
||||||
|
|
||||||
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient, is_multiplayer: bool = false) -> void:
|
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient, is_multiplayer: bool = false, card_size: Vector2 = Vector2(300, 400)) -> void:
|
||||||
var btn = TextureButton.new()
|
var btn = TextureButton.new()
|
||||||
btn.custom_minimum_size = Vector2(300, 400)
|
btn.custom_minimum_size = card_size
|
||||||
|
|
||||||
var tex = GradientTexture2D.new()
|
var tex = GradientTexture2D.new()
|
||||||
tex.gradient = gradient
|
tex.gradient = gradient
|
||||||
tex.width = 300
|
tex.width = int(card_size.x)
|
||||||
tex.height = 400
|
tex.height = int(card_size.y)
|
||||||
tex.fill_to = Vector2(1, 1)
|
tex.fill_to = Vector2(1, 1)
|
||||||
btn.texture_normal = tex
|
btn.texture_normal = tex
|
||||||
|
|
||||||
var tex_hover = GradientTexture2D.new()
|
var tex_hover = GradientTexture2D.new()
|
||||||
tex_hover.gradient = gradient
|
tex_hover.gradient = gradient
|
||||||
tex_hover.width = 300
|
tex_hover.width = int(card_size.x)
|
||||||
tex_hover.height = 400
|
tex_hover.height = int(card_size.y)
|
||||||
tex_hover.fill_to = Vector2(1, 1)
|
tex_hover.fill_to = Vector2(1, 1)
|
||||||
tex_hover.use_hdr = true # Slight bright effect on hover
|
tex_hover.use_hdr = true # Slight bright effect on hover
|
||||||
btn.texture_hover = tex_hover
|
btn.texture_hover = tex_hover
|
||||||
@@ -294,7 +308,12 @@ func _build_level_card(parent: Container, title: String, scene_path: String, gra
|
|||||||
if is_multiplayer:
|
if is_multiplayer:
|
||||||
btn.pressed.connect(func(): get_node("/root/NetworkManager").rpc_load_level.rpc(scene_path))
|
btn.pressed.connect(func(): get_node("/root/NetworkManager").rpc_load_level.rpc(scene_path))
|
||||||
else:
|
else:
|
||||||
btn.pressed.connect(func(): get_tree().change_scene_to_file(scene_path))
|
btn.pressed.connect(func():
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm:
|
||||||
|
nm.start_singleplayer_match("Deathmatch")
|
||||||
|
get_tree().change_scene_to_file(scene_path)
|
||||||
|
)
|
||||||
|
|
||||||
parent.add_child(btn)
|
parent.add_child(btn)
|
||||||
|
|
||||||
@@ -332,6 +351,8 @@ func _on_host_pressed() -> void:
|
|||||||
if not nm.player_connected.is_connected(_on_network_player_connected):
|
if not nm.player_connected.is_connected(_on_network_player_connected):
|
||||||
nm.player_connected.connect(_on_network_player_connected)
|
nm.player_connected.connect(_on_network_player_connected)
|
||||||
nm.player_disconnected.connect(_on_network_player_disconnected)
|
nm.player_disconnected.connect(_on_network_player_disconnected)
|
||||||
|
if not nm.stats_updated.is_connected(_update_lobby_players):
|
||||||
|
nm.stats_updated.connect(_update_lobby_players)
|
||||||
|
|
||||||
func _on_direct_connect_pressed() -> void:
|
func _on_direct_connect_pressed() -> void:
|
||||||
_multiplayer_panel.hide()
|
_multiplayer_panel.hide()
|
||||||
@@ -355,6 +376,8 @@ func _on_lobby_back_pressed() -> void:
|
|||||||
if nm.player_connected.is_connected(_on_network_player_connected):
|
if nm.player_connected.is_connected(_on_network_player_connected):
|
||||||
nm.player_connected.disconnect(_on_network_player_connected)
|
nm.player_connected.disconnect(_on_network_player_connected)
|
||||||
nm.player_disconnected.disconnect(_on_network_player_disconnected)
|
nm.player_disconnected.disconnect(_on_network_player_disconnected)
|
||||||
|
if nm.stats_updated.is_connected(_update_lobby_players):
|
||||||
|
nm.stats_updated.disconnect(_update_lobby_players)
|
||||||
|
|
||||||
func _on_network_player_connected(_id: int) -> void:
|
func _on_network_player_connected(_id: int) -> void:
|
||||||
_update_lobby_players()
|
_update_lobby_players()
|
||||||
@@ -367,11 +390,19 @@ func _update_lobby_players() -> void:
|
|||||||
_host_lobby_players_list.clear()
|
_host_lobby_players_list.clear()
|
||||||
var nm = get_node("/root/NetworkManager")
|
var nm = get_node("/root/NetworkManager")
|
||||||
var host_name = SettingsManager.username
|
var host_name = SettingsManager.username
|
||||||
|
var host_ping = 0
|
||||||
|
if nm.player_stats.has(1): host_ping = nm.player_stats[1].get("ping", 0)
|
||||||
|
|
||||||
for pid in nm.connected_players:
|
for pid in nm.connected_players:
|
||||||
|
var p_ping = 0
|
||||||
|
if nm.player_stats.has(pid): p_ping = nm.player_stats[pid].get("ping", 0)
|
||||||
|
|
||||||
if pid == 1:
|
if pid == 1:
|
||||||
_host_lobby_players_list.add_item(host_name + " (Host)")
|
_host_lobby_players_list.add_item("%s (Host) - %sms" % [host_name, host_ping])
|
||||||
else:
|
else:
|
||||||
_host_lobby_players_list.add_item("Player " + str(pid))
|
var p_name = "Player " + str(pid)
|
||||||
|
if nm.player_stats.has(pid): p_name = nm.player_stats[pid].get("username", p_name)
|
||||||
|
_host_lobby_players_list.add_item("%s - %sms" % [p_name, p_ping])
|
||||||
|
|
||||||
func _on_loadouts_pressed() -> void:
|
func _on_loadouts_pressed() -> void:
|
||||||
if has_node("/root/PauseMenu"):
|
if has_node("/root/PauseMenu"):
|
||||||
|
|||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
var _nm: Node
|
||||||
|
|
||||||
|
# Top HUD
|
||||||
|
var _time_label: Label
|
||||||
|
var _local_kills_label: Label
|
||||||
|
var _leader_label: Label
|
||||||
|
|
||||||
|
# Killfeed
|
||||||
|
var _killfeed_vbox: VBoxContainer
|
||||||
|
|
||||||
|
# Scoreboard
|
||||||
|
var _scoreboard_panel: PanelContainer
|
||||||
|
var _scoreboard_grid: GridContainer
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
|
||||||
|
# Top HUD
|
||||||
|
var top_hbox = HBoxContainer.new()
|
||||||
|
top_hbox.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||||
|
top_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
top_hbox.add_theme_constant_override("separation", 50)
|
||||||
|
var margin = MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
margin.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||||
|
margin.add_child(top_hbox)
|
||||||
|
add_child(margin)
|
||||||
|
|
||||||
|
_time_label = Label.new()
|
||||||
|
_time_label.add_theme_font_size_override("font_size", 48)
|
||||||
|
_time_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
_time_label.add_theme_constant_override("outline_size", 8)
|
||||||
|
top_hbox.add_child(_time_label)
|
||||||
|
|
||||||
|
_local_kills_label = Label.new()
|
||||||
|
_local_kills_label.text = "0 Kills"
|
||||||
|
_local_kills_label.add_theme_font_size_override("font_size", 64)
|
||||||
|
_local_kills_label.add_theme_color_override("font_color", Color(1, 0.8, 0.2))
|
||||||
|
_local_kills_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
_local_kills_label.add_theme_constant_override("outline_size", 12)
|
||||||
|
top_hbox.add_child(_local_kills_label)
|
||||||
|
|
||||||
|
_leader_label = Label.new()
|
||||||
|
_leader_label.text = "Leader: None"
|
||||||
|
_leader_label.add_theme_font_size_override("font_size", 32)
|
||||||
|
_leader_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
_leader_label.add_theme_constant_override("outline_size", 8)
|
||||||
|
top_hbox.add_child(_leader_label)
|
||||||
|
|
||||||
|
# Killfeed
|
||||||
|
var kf_margin = MarginContainer.new()
|
||||||
|
kf_margin.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||||
|
kf_margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
kf_margin.add_theme_constant_override("margin_right", 20)
|
||||||
|
add_child(kf_margin)
|
||||||
|
|
||||||
|
_killfeed_vbox = VBoxContainer.new()
|
||||||
|
_killfeed_vbox.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
kf_margin.add_child(_killfeed_vbox)
|
||||||
|
|
||||||
|
# Scoreboard Overlay
|
||||||
|
var center_container = CenterContainer.new()
|
||||||
|
center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(center_container)
|
||||||
|
|
||||||
|
_scoreboard_panel = PanelContainer.new()
|
||||||
|
_scoreboard_panel.custom_minimum_size = Vector2(800, 600)
|
||||||
|
var sb_style = StyleBoxFlat.new()
|
||||||
|
sb_style.bg_color = Color(0, 0, 0, 0.8)
|
||||||
|
_scoreboard_panel.add_theme_stylebox_override("panel", sb_style)
|
||||||
|
_scoreboard_panel.hide()
|
||||||
|
center_container.add_child(_scoreboard_panel)
|
||||||
|
|
||||||
|
var sb_vbox = VBoxContainer.new()
|
||||||
|
var sb_margin = MarginContainer.new()
|
||||||
|
sb_margin.add_theme_constant_override("margin_left", 20)
|
||||||
|
sb_margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
sb_margin.add_theme_constant_override("margin_right", 20)
|
||||||
|
sb_margin.add_theme_constant_override("margin_bottom", 20)
|
||||||
|
sb_margin.add_child(sb_vbox)
|
||||||
|
_scoreboard_panel.add_child(sb_margin)
|
||||||
|
|
||||||
|
var sb_title = Label.new()
|
||||||
|
sb_title.text = "Scoreboard"
|
||||||
|
sb_title.add_theme_font_size_override("font_size", 48)
|
||||||
|
sb_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
sb_vbox.add_child(sb_title)
|
||||||
|
|
||||||
|
_scoreboard_grid = GridContainer.new()
|
||||||
|
_scoreboard_grid.columns = 5
|
||||||
|
_scoreboard_grid.add_theme_constant_override("h_separation", 50)
|
||||||
|
_scoreboard_grid.add_theme_constant_override("v_separation", 10)
|
||||||
|
sb_vbox.add_child(_scoreboard_grid)
|
||||||
|
|
||||||
|
_populate_scoreboard_headers()
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
if _nm:
|
||||||
|
_nm.stats_updated.connect(_update_hud)
|
||||||
|
_nm.killfeed_event.connect(_on_killfeed_event)
|
||||||
|
_nm.match_state_updated.connect(_update_timer)
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if Input.is_action_just_pressed("scoreboard"):
|
||||||
|
_scoreboard_panel.show()
|
||||||
|
_update_hud()
|
||||||
|
elif Input.is_action_just_released("scoreboard"):
|
||||||
|
_scoreboard_panel.hide()
|
||||||
|
|
||||||
|
if _nm and _nm.match_active:
|
||||||
|
_update_timer()
|
||||||
|
|
||||||
|
func _update_timer() -> void:
|
||||||
|
if not _nm: return
|
||||||
|
var t = int(_nm.match_time_remaining)
|
||||||
|
var m = int(t / 60.0)
|
||||||
|
var s = t % 60
|
||||||
|
_time_label.text = "%02d:%02d" % [m, s]
|
||||||
|
|
||||||
|
func _populate_scoreboard_headers() -> void:
|
||||||
|
for child in _scoreboard_grid.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
var headers = ["Player", "Kills", "Deaths", "Assists", "Ping"]
|
||||||
|
for h in headers:
|
||||||
|
var l = Label.new()
|
||||||
|
l.text = h
|
||||||
|
l.add_theme_font_size_override("font_size", 24)
|
||||||
|
l.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||||
|
_scoreboard_grid.add_child(l)
|
||||||
|
|
||||||
|
func _update_hud() -> void:
|
||||||
|
if not _nm: return
|
||||||
|
|
||||||
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
|
var leader_name = "None"
|
||||||
|
var max_kills = -1
|
||||||
|
|
||||||
|
var stats = _nm.player_stats
|
||||||
|
|
||||||
|
# Sort players by kills for scoreboard
|
||||||
|
var players = stats.keys()
|
||||||
|
players.sort_custom(func(a, b): return stats[a].get("kills", 0) > stats[b].get("kills", 0))
|
||||||
|
|
||||||
|
_populate_scoreboard_headers()
|
||||||
|
|
||||||
|
for pid in players:
|
||||||
|
var p_data = stats[pid]
|
||||||
|
var p_kills = p_data.get("kills", 0)
|
||||||
|
|
||||||
|
# Find leader
|
||||||
|
if p_kills > max_kills:
|
||||||
|
max_kills = p_kills
|
||||||
|
leader_name = p_data.get("username", "Player " + str(pid))
|
||||||
|
|
||||||
|
# Update local kills
|
||||||
|
if pid == my_id:
|
||||||
|
_local_kills_label.text = str(p_kills) + " Kills"
|
||||||
|
|
||||||
|
# Build Scoreboard Row
|
||||||
|
var c_name = Label.new()
|
||||||
|
c_name.text = p_data.get("username", "Player")
|
||||||
|
c_name.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_name)
|
||||||
|
|
||||||
|
var c_k = Label.new()
|
||||||
|
c_k.text = str(p_kills)
|
||||||
|
c_k.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_k)
|
||||||
|
|
||||||
|
var c_d = Label.new()
|
||||||
|
c_d.text = str(p_data.get("deaths", 0))
|
||||||
|
c_d.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_d)
|
||||||
|
|
||||||
|
var c_a = Label.new()
|
||||||
|
c_a.text = str(p_data.get("assists", 0))
|
||||||
|
c_a.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_a)
|
||||||
|
|
||||||
|
var c_p = Label.new()
|
||||||
|
c_p.text = str(p_data.get("ping", 0)) + "ms"
|
||||||
|
c_p.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_p)
|
||||||
|
|
||||||
|
_leader_label.text = "Leader: %s (%s Kills)" % [leader_name, max_kills]
|
||||||
|
|
||||||
|
func _on_killfeed_event(victim: String, killer: String, weapon: String) -> void:
|
||||||
|
var l = Label.new()
|
||||||
|
if killer == "":
|
||||||
|
l.text = "%s died." % victim
|
||||||
|
else:
|
||||||
|
l.text = "%s [%s] %s" % [killer, weapon, victim]
|
||||||
|
|
||||||
|
l.add_theme_font_size_override("font_size", 20)
|
||||||
|
l.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
l.add_theme_constant_override("outline_size", 4)
|
||||||
|
_killfeed_vbox.add_child(l)
|
||||||
|
|
||||||
|
# Fade out and queue_free after 5 seconds
|
||||||
|
var t = get_tree().create_tween()
|
||||||
|
t.tween_interval(4.0)
|
||||||
|
t.tween_property(l, "modulate:a", 0.0, 1.0)
|
||||||
|
t.tween_callback(l.queue_free)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dymuoha2tdj63
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3 uid="uid://c54h8k8u8p7q6"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/match_hud.gd" id="1_match"]
|
||||||
|
|
||||||
|
[node name="MatchHUD" type="CanvasLayer"]
|
||||||
|
script = ExtResource("1_match")
|
||||||
Reference in New Issue
Block a user