247 lines
8.4 KiB
GDScript
247 lines
8.4 KiB
GDScript
extends Node
|
|
|
|
signal player_connected(id: int)
|
|
signal player_disconnected(id: int)
|
|
signal connection_failed
|
|
signal connection_succeeded
|
|
signal match_state_updated
|
|
signal stats_updated
|
|
signal killfeed_event(victim: String, killer: String, weapon: String, v_color: String, k_color: String)
|
|
|
|
const DEFAULT_PORT = 31415
|
|
|
|
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:
|
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
|
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
|
multiplayer.connection_failed.connect(_on_connection_failed)
|
|
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:
|
|
var peer = ENetMultiplayerPeer.new()
|
|
var err = peer.create_server(port)
|
|
if err == OK:
|
|
multiplayer.multiplayer_peer = peer
|
|
connected_players.clear()
|
|
connected_players.append(1) # Host is 1
|
|
_init_player_stats(1, SettingsManager.username)
|
|
_ping_timer.start()
|
|
print("Hosting on port %s" % port)
|
|
return err
|
|
|
|
func join_game(ip: String, port: int = DEFAULT_PORT) -> Error:
|
|
var peer = ENetMultiplayerPeer.new()
|
|
var err = peer.create_client(ip, port)
|
|
if err == OK:
|
|
multiplayer.multiplayer_peer = peer
|
|
print("Connecting to %s:%s" % [ip, port])
|
|
return err
|
|
|
|
func disconnect_game() -> void:
|
|
multiplayer.multiplayer_peer = null
|
|
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:
|
|
# Keep existing color if player re-joins, or generate new
|
|
var p_color = Color(randf_range(0.2, 1.0), randf_range(0.2, 1.0), randf_range(0.2, 1.0)).to_html(false)
|
|
|
|
player_stats[id] = {
|
|
"username": username,
|
|
"kills": 0,
|
|
"deaths": 0,
|
|
"assists": 0,
|
|
"ping": 0,
|
|
"color": p_color
|
|
}
|
|
stats_updated.emit()
|
|
|
|
func _on_peer_connected(id: int) -> void:
|
|
print("Player connected: ", id)
|
|
if not connected_players.has(id):
|
|
connected_players.append(id)
|
|
|
|
# If server, we wait for the client to register themselves via RPC
|
|
pass
|
|
|
|
func _on_peer_disconnected(id: int) -> void:
|
|
print("Player disconnected: ", id)
|
|
connected_players.erase(id)
|
|
if player_stats.has(id):
|
|
player_stats.erase(id)
|
|
player_disconnected.emit(id)
|
|
if multiplayer.is_server():
|
|
rpc("sync_player_stats", player_stats)
|
|
|
|
func _on_connected_to_server() -> void:
|
|
print("Successfully connected to server")
|
|
connection_succeeded.emit()
|
|
# Tell the server our username
|
|
rpc_id(1, "register_player", SettingsManager.username)
|
|
|
|
func _on_connection_failed() -> void:
|
|
print("Failed to connect to server")
|
|
multiplayer.multiplayer_peer = null
|
|
connection_failed.emit()
|
|
|
|
func _on_server_disconnected() -> void:
|
|
print("Server disconnected")
|
|
multiplayer.multiplayer_peer = null
|
|
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("any_peer", "call_local", "reliable")
|
|
func update_username(new_name: String) -> void:
|
|
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
|
var sender_id = multiplayer.get_remote_sender_id()
|
|
if sender_id == 0:
|
|
sender_id = 1
|
|
if player_stats.has(sender_id):
|
|
player_stats[sender_id]["username"] = new_name
|
|
stats_updated.emit()
|
|
if multiplayer.has_multiplayer_peer() and not multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
|
rpc("sync_player_stats", player_stats)
|
|
|
|
@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")
|
|
func rpc_load_level(scene_path: String) -> void:
|
|
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)
|
|
|
|
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 = "", assist_ids: Array = []) -> void:
|
|
if multiplayer.is_server():
|
|
if killer_id != 0 and killer_id != victim_id:
|
|
if player_stats.has(killer_id):
|
|
player_stats[killer_id]["kills"] += 1
|
|
|
|
if victim_id != -1:
|
|
if player_stats.has(victim_id):
|
|
player_stats[victim_id]["deaths"] += 1
|
|
|
|
for aid in assist_ids:
|
|
if player_stats.has(aid) and aid != killer_id and aid != victim_id:
|
|
player_stats[aid]["assists"] += 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"
|
|
|
|
var v_color = player_stats[victim_id].get("color", "cccccc") if player_stats.has(victim_id) else "cccccc"
|
|
var k_color = player_stats[killer_id].get("color", "cccccc") if player_stats.has(killer_id) else "cccccc"
|
|
|
|
# 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
|
|
k_color = "55aaff" # Default blueish for uninitialized local player
|
|
|
|
if victim_id == -1 and "Dummy" in custom_victim_name:
|
|
v_color = "ff5555" # Red for dummies
|
|
|
|
if killer_id == 0 or killer_id == victim_id:
|
|
k_name = "" # Suicide or world death
|
|
|
|
broadcast_killfeed.rpc(v_name, k_name, weapon_name, v_color, k_color)
|
|
sync_player_stats.rpc(player_stats)
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func broadcast_killfeed(victim: String, killer: String, weapon: String, v_color: String = "cccccc", k_color: String = "cccccc") -> void:
|
|
killfeed_event.emit(victim, killer, weapon, v_color, k_color)
|