Compare commits

...
5 Commits
15 changed files with 940 additions and 25 deletions
+1
View File
@@ -71,6 +71,7 @@ func _create_ui() -> void:
_ui_overlay = ColorRect.new() _ui_overlay = ColorRect.new()
_ui_overlay.set_anchors_preset(Control.PRESET_FULL_RECT) _ui_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
_ui_overlay.color = Color(1.0, 0.0, 0.0, 0.0) _ui_overlay.color = Color(1.0, 0.0, 0.0, 0.0)
_ui_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
_ui_layer.add_child(_ui_overlay) _ui_layer.add_child(_ui_overlay)
_ui_label = Label.new() _ui_label = Label.new()
+36
View File
@@ -13,6 +13,7 @@ var shield: float = 100.0
var is_dead: bool = false var is_dead: bool = false
var ragdoll_instance: Node3D = null var ragdoll_instance: Node3D = null
var pending_impulse: Vector3 = Vector3.ZERO var pending_impulse: Vector3 = Vector3.ZERO
var recent_attackers: Dictionary = {}
func _ready() -> void: func _ready() -> void:
add_to_group("enemies") add_to_group("enemies")
@@ -42,6 +43,18 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
if is_dead: if is_dead:
return return
var attacker_id = 0
if source:
if source.has_method("get_multiplayer_authority"):
attacker_id = source.get_multiplayer_authority()
elif "owner_player" in source and source.owner_player:
attacker_id = int(str(source.owner_player.name))
elif source is CharacterBody3D:
attacker_id = int(str(source.name))
if attacker_id != 0:
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
var dmg_taken = amount var dmg_taken = amount
if shield > 0: if shield > 0:
if shield >= amount: if shield >= amount:
@@ -61,6 +74,29 @@ 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()
var assist_ids: Array = []
var now = Time.get_ticks_msec() / 1000.0
for aid in recent_attackers.keys():
if now - recent_attackers[aid] <= 10.0:
assist_ids.append(aid)
nm.register_kill.rpc(-1, killer_id, weapon_name, "Target Dummy", assist_ids)
else: else:
_wiggle() _wiggle()
+36
View File
@@ -11,6 +11,7 @@ var max_shield: float = 100.0
var shield: float = 100.0 var shield: float = 100.0
var is_dead: bool = false var is_dead: bool = false
var recent_attackers: Dictionary = {} # attacker_id: timestamp
var ragdoll_instance: Node3D = null var ragdoll_instance: Node3D = null
var pending_impulse: Vector3 = Vector3.ZERO var pending_impulse: Vector3 = Vector3.ZERO
@@ -85,6 +86,18 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
if is_dead: if is_dead:
return return
var attacker_id = 0
if source:
if source.has_method("get_multiplayer_authority"):
attacker_id = source.get_multiplayer_authority()
elif "owner_player" in source and source.owner_player:
attacker_id = int(str(source.owner_player.name))
elif source is CharacterBody3D:
attacker_id = int(str(source.name))
if attacker_id != 0:
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
var dmg_taken = amount var dmg_taken = amount
if shield > 0: if shield > 0:
if shield >= amount: if shield >= amount:
@@ -104,6 +117,29 @@ 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()
var assist_ids: Array = []
var now = Time.get_ticks_msec() / 1000.0
for aid in recent_attackers.keys():
if now - recent_attackers[aid] <= 10.0:
assist_ids.append(aid)
nm.register_kill.rpc(-1, killer_id, weapon_name, "Walking Dummy", assist_ids)
else: else:
_wiggle() _wiggle()
+234
View File
@@ -0,0 +1,234 @@
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("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)
+1
View File
@@ -0,0 +1 @@
uid://bcq67g48h8hka
+6 -6
View File
@@ -26,6 +26,7 @@ var default_bindings = {
var current_bindings = {} var current_bindings = {}
var show_debug_ui: bool = false var show_debug_ui: bool = false
var username: String = "Player"
var mouse_sensitivity: float = 0.002 var mouse_sensitivity: float = 0.002
var ads_sensitivity: float = 0.001 var ads_sensitivity: float = 0.001
@@ -131,6 +132,7 @@ func _save_settings() -> void:
var save_data = { var save_data = {
"bindings": current_bindings, "bindings": current_bindings,
"show_debug_ui": show_debug_ui, "show_debug_ui": show_debug_ui,
"username": username,
"mouse_sensitivity": mouse_sensitivity, "mouse_sensitivity": mouse_sensitivity,
"ads_sensitivity": ads_sensitivity, "ads_sensitivity": ads_sensitivity,
"display_mode": display_mode, "display_mode": display_mode,
@@ -165,12 +167,10 @@ func _load_settings() -> void:
for action in loaded_bindings: for action in loaded_bindings:
if current_bindings.has(action): if current_bindings.has(action):
current_bindings[action] = loaded_bindings[action] current_bindings[action] = loaded_bindings[action]
if data.has("show_debug_ui"): if data.has("username"): username = data["username"]
show_debug_ui = data["show_debug_ui"] if data.has("show_debug_ui"): show_debug_ui = data["show_debug_ui"]
if data.has("mouse_sensitivity"): if data.has("mouse_sensitivity"): mouse_sensitivity = data["mouse_sensitivity"]
mouse_sensitivity = data["mouse_sensitivity"] if data.has("ads_sensitivity"): ads_sensitivity = data["ads_sensitivity"]
if data.has("ads_sensitivity"):
ads_sensitivity = data["ads_sensitivity"]
if data.has("display_mode"): display_mode = data["display_mode"] if data.has("display_mode"): display_mode = data["display_mode"]
if data.has("resolution_index"): resolution_index = data["resolution_index"] if data.has("resolution_index"): resolution_index = data["resolution_index"]
if data.has("fps_cap"): fps_cap = data["fps_cap"] if data.has("fps_cap"): fps_cap = data["fps_cap"]
+86 -7
View File
@@ -26,10 +26,10 @@ var shield_label: Label
var death_screen: Control var death_screen: Control
var ragdoll_instance: Node3D var ragdoll_instance: Node3D
# Audio # Audio
var footstep_player: AudioStreamPlayer var footstep_player: AudioStreamPlayer
var dash_player: AudioStreamPlayer var dash_player: AudioStreamPlayer
var recent_attackers: Dictionary = {} # attacker_id: timestamp
var hit_player: AudioStreamPlayer var hit_player: AudioStreamPlayer
var slide_player: AudioStreamPlayer var slide_player: AudioStreamPlayer
var wallrun_player: AudioStreamPlayer var wallrun_player: AudioStreamPlayer
@@ -227,6 +227,19 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
# Show damage number for self-damage # Show damage number for self-damage
spawn_damage_number(amount, _hit_pos) spawn_damage_number(amount, _hit_pos)
# Handle damage tracking for assists
var attacker_id = 0
if _source:
if _source.has_method("get_multiplayer_authority"):
attacker_id = _source.get_multiplayer_authority()
elif "owner_player" in _source and _source.owner_player:
attacker_id = int(str(_source.owner_player.name))
elif _source is CharacterBody3D:
attacker_id = int(str(_source.name))
if attacker_id != 0:
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
time_since_last_damage = 0.0 time_since_last_damage = 0.0
if shield > 0.0: if shield > 0.0:
@@ -243,6 +256,49 @@ 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"
var assist_ids: Array = []
var now = Time.get_ticks_msec() / 1000.0
for aid in recent_attackers.keys():
if aid != killer_id and now - recent_attackers[aid] <= 10.0:
assist_ids.append(aid)
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, "", assist_ids)
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:
var p = ""
if w.has_meta("script_path"):
p = w.get_meta("script_path").get_file().get_basename()
elif w.get_script():
p = w.get_script().resource_path.get_file().get_basename()
else:
p = "Killed"
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
@@ -354,6 +410,11 @@ func _process(delta: float) -> void:
if is_instance_valid(spring_arm): if is_instance_valid(spring_arm):
# Follow torso position exactly to avoid lerp bobbing # Follow torso position exactly to avoid lerp bobbing
spring_arm.global_position = torso.global_position spring_arm.global_position = torso.global_position
if death_screen and death_screen.visible:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) or Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or Input.is_key_pressed(KEY_SPACE) or Input.is_key_pressed(KEY_ENTER) or Input.is_key_pressed(KEY_ESCAPE) == false and (Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("jump") or Input.is_action_just_pressed("fire")):
_on_respawn_pressed()
return return
# Shield recharge logic # Shield recharge logic
@@ -381,6 +442,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()
@@ -458,11 +526,15 @@ func _setup_hud() -> void:
d_title.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2)) d_title.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2))
d_vbox.add_child(d_title) d_vbox.add_child(d_title)
var respawn_btn = Button.new() var respawn_label = Label.new()
respawn_btn.text = "REBOOT" respawn_label.text = "PRESS ANY KEY TO REBOOT"
respawn_btn.add_theme_font_size_override("font_size", 24) respawn_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
respawn_btn.pressed.connect(_on_respawn_pressed) respawn_label.add_theme_font_size_override("font_size", 24)
d_vbox.add_child(respawn_btn) d_vbox.add_child(respawn_label)
var tw = create_tween().set_loops()
tw.tween_property(respawn_label, "modulate:a", 0.2, 0.8)
tw.tween_property(respawn_label, "modulate:a", 1.0, 0.8)
_damage_layer.add_child(death_screen) _damage_layer.add_child(death_screen)
@@ -503,6 +575,7 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
var wman = camera.get_node_or_null("WeaponManager") var wman = camera.get_node_or_null("WeaponManager")
if wman: if wman:
wman.visible = false wman.visible = false
wman.process_mode = Node.PROCESS_MODE_DISABLED
var cam_trans = camera.global_transform var cam_trans = camera.global_transform
camera.get_parent().remove_child(camera) camera.get_parent().remove_child(camera)
@@ -556,7 +629,13 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
# Show death screen # Show death screen
death_screen.visible = true death_screen.visible = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE set_process_input(true)
func _input(event: InputEvent) -> void:
if is_dead and death_screen and death_screen.visible:
if event is InputEventKey or event is InputEventMouseButton or event is InputEventJoypadButton:
if event.is_pressed() and not event.is_echo():
_on_respawn_pressed()
func _on_respawn_pressed() -> void: func _on_respawn_pressed() -> void:
# Reload scene # Reload scene
+6
View File
@@ -20,6 +20,7 @@ config/icon="res://icon.svg"
SettingsManager="*res://globals/settings_manager.gd" SettingsManager="*res://globals/settings_manager.gd"
LoadoutManager="*res://globals/loadout_manager.gd" LoadoutManager="*res://globals/loadout_manager.gd"
PauseMenu="*res://ui/pause_menu.gd" PauseMenu="*res://ui/pause_menu.gd"
NetworkManager="*res://globals/network_manager.gd"
[input] [input]
@@ -88,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
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://clrd47khl65oq
+258 -8
View File
@@ -2,6 +2,10 @@ extends Control
var _ui_vbox: VBoxContainer var _ui_vbox: VBoxContainer
var _level_selector_panel: VBoxContainer var _level_selector_panel: VBoxContainer
var _multiplayer_panel: VBoxContainer
var _direct_connect_panel: VBoxContainer
var _host_lobby_panel: Control
var _host_lobby_players_list: ItemList
func _ready() -> void: func _ready() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
@@ -97,21 +101,192 @@ func _ready() -> void:
_add_button(_level_selector_panel, "Back", _on_back_pressed) _add_button(_level_selector_panel, "Back", _on_back_pressed)
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient) -> void: # ==========================================
# MULTIPLAYER PANEL
# ==========================================
_multiplayer_panel = VBoxContainer.new()
_multiplayer_panel.add_theme_constant_override("separation", 20)
_multiplayer_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
_multiplayer_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
_multiplayer_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600)
_multiplayer_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50)
_multiplayer_panel.hide()
add_child(_multiplayer_panel)
var mp_title = Label.new()
mp_title.text = "Multiplayer"
mp_title.add_theme_font_size_override("font_size", 64)
mp_title.add_theme_color_override("font_color", Color(1, 1, 1))
mp_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
mp_title.add_theme_constant_override("outline_size", 8)
_multiplayer_panel.add_child(mp_title)
var mp_spacer = Control.new()
mp_spacer.custom_minimum_size = Vector2(0, 20)
_multiplayer_panel.add_child(mp_spacer)
_add_button(_multiplayer_panel, "Host Game", _on_host_pressed)
_add_button(_multiplayer_panel, "Direct Connect", _on_direct_connect_pressed)
_add_button(_multiplayer_panel, "Back", _on_multiplayer_back_pressed)
# ==========================================
# DIRECT CONNECT PANEL
# ==========================================
_direct_connect_panel = VBoxContainer.new()
_direct_connect_panel.add_theme_constant_override("separation", 20)
_direct_connect_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
_direct_connect_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
_direct_connect_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600)
_direct_connect_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50)
_direct_connect_panel.hide()
add_child(_direct_connect_panel)
var dc_title = Label.new()
dc_title.text = "Direct Connect"
dc_title.add_theme_font_size_override("font_size", 64)
dc_title.add_theme_color_override("font_color", Color(1, 1, 1))
dc_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
dc_title.add_theme_constant_override("outline_size", 8)
_direct_connect_panel.add_child(dc_title)
var dc_ip_label = Label.new()
dc_ip_label.text = "IP Address:"
dc_ip_label.add_theme_font_size_override("font_size", 24)
_direct_connect_panel.add_child(dc_ip_label)
var dc_ip_input = LineEdit.new()
dc_ip_input.text = "127.0.0.1"
dc_ip_input.name = "IPInput"
dc_ip_input.add_theme_font_size_override("font_size", 24)
dc_ip_input.custom_minimum_size = Vector2(300, 50)
_direct_connect_panel.add_child(dc_ip_input)
var dc_port_label = Label.new()
dc_port_label.text = "Port:"
dc_port_label.add_theme_font_size_override("font_size", 24)
_direct_connect_panel.add_child(dc_port_label)
var dc_port_input = LineEdit.new()
dc_port_input.text = "31415" # Default Port
dc_port_input.name = "PortInput"
dc_port_input.add_theme_font_size_override("font_size", 24)
dc_port_input.custom_minimum_size = Vector2(300, 50)
_direct_connect_panel.add_child(dc_port_input)
var dc_spacer = Control.new()
dc_spacer.custom_minimum_size = Vector2(0, 20)
_direct_connect_panel.add_child(dc_spacer)
_add_button(_direct_connect_panel, "Connect", _on_connect_confirm_pressed)
_add_button(_direct_connect_panel, "Back", _on_dc_back_pressed)
# ==========================================
# HOST LOBBY PANEL
# ==========================================
_host_lobby_panel = Control.new()
_host_lobby_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
_host_lobby_panel.hide()
add_child(_host_lobby_panel)
var hl_hbox = HBoxContainer.new()
hl_hbox.set_anchors_preset(Control.PRESET_FULL_RECT)
hl_hbox.add_theme_constant_override("separation", 50)
_host_lobby_panel.add_child(hl_hbox)
# Left Side (Level Selector)
var hl_left = VBoxContainer.new()
hl_left.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hl_left.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
hl_left.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
hl_left.add_theme_constant_override("separation", 20)
var hl_margin = MarginContainer.new()
hl_margin.add_theme_constant_override("margin_left", 50)
hl_margin.add_theme_constant_override("margin_top", 50)
hl_margin.add_theme_constant_override("margin_bottom", 50)
hl_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hl_hbox.add_child(hl_margin)
hl_margin.add_child(hl_left)
var hl_title = Label.new()
hl_title.text = "Host Lobby - Select Level"
hl_title.add_theme_font_size_override("font_size", 64)
hl_title.add_theme_color_override("font_color", Color(1, 1, 1))
hl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
hl_title.add_theme_constant_override("outline_size", 8)
hl_left.add_child(hl_title)
var hl_cards = HBoxContainer.new()
hl_cards.add_theme_constant_override("separation", 20)
hl_left.add_child(hl_cards)
_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, 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()
hl_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
hl_left.add_child(hl_spacer)
_add_button(hl_left, "Disband & Back", _on_lobby_back_pressed)
# Right Side (Player List)
var hl_right = VBoxContainer.new()
hl_right.custom_minimum_size = Vector2(400, 0)
var hl_right_margin = MarginContainer.new()
hl_right_margin.add_theme_constant_override("margin_top", 50)
hl_right_margin.add_theme_constant_override("margin_right", 50)
hl_right_margin.add_theme_constant_override("margin_bottom", 50)
hl_hbox.add_child(hl_right_margin)
hl_right_margin.add_child(hl_right)
var pl_title = Label.new()
pl_title.text = "Players Connected"
pl_title.add_theme_font_size_override("font_size", 48)
pl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
pl_title.add_theme_constant_override("outline_size", 8)
hl_right.add_child(pl_title)
var port_label = Label.new()
if get_node_or_null("/root/NetworkManager"):
port_label.text = "Port: " + str(get_node("/root/NetworkManager").DEFAULT_PORT)
port_label.add_theme_font_size_override("font_size", 24)
port_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
hl_right.add_child(port_label)
_host_lobby_players_list = ItemList.new()
_host_lobby_players_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
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, 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
@@ -137,7 +312,15 @@ func _build_level_card(parent: Container, title: String, scene_path: String, gra
lbl.set_anchors_preset(Control.PRESET_FULL_RECT) lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
overlay.add_child(lbl) overlay.add_child(lbl)
btn.pressed.connect(func(): get_tree().change_scene_to_file(scene_path)) if is_multiplayer:
btn.pressed.connect(func(): get_node("/root/NetworkManager").rpc_load_level.rpc(scene_path))
else:
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)
@@ -159,7 +342,74 @@ func _on_singleplayer_pressed() -> void:
_level_selector_panel.show() _level_selector_panel.show()
func _on_multiplayer_pressed() -> void: func _on_multiplayer_pressed() -> void:
print("Multiplayer not implemented yet.") _ui_vbox.hide()
_multiplayer_panel.show()
func _on_multiplayer_back_pressed() -> void:
_multiplayer_panel.hide()
_ui_vbox.show()
func _on_host_pressed() -> void:
_multiplayer_panel.hide()
_host_lobby_panel.show()
var nm = get_node("/root/NetworkManager")
nm.host_game()
_update_lobby_players()
if not nm.player_connected.is_connected(_on_network_player_connected):
nm.player_connected.connect(_on_network_player_connected)
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:
_multiplayer_panel.hide()
_direct_connect_panel.show()
func _on_dc_back_pressed() -> void:
_direct_connect_panel.hide()
_multiplayer_panel.show()
func _on_connect_confirm_pressed() -> void:
var ip = _direct_connect_panel.get_node("IPInput").text
var port = _direct_connect_panel.get_node("PortInput").text.to_int()
get_node("/root/NetworkManager").join_game(ip, port)
# For simplicity, we can show a connecting label or just wait for the game to start via RPC
func _on_lobby_back_pressed() -> void:
_host_lobby_panel.hide()
_multiplayer_panel.show()
var nm = get_node("/root/NetworkManager")
nm.disconnect_game()
if nm.player_connected.is_connected(_on_network_player_connected):
nm.player_connected.disconnect(_on_network_player_connected)
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:
_update_lobby_players()
func _on_network_player_disconnected(_id: int) -> void:
_update_lobby_players()
func _update_lobby_players() -> void:
if not _host_lobby_players_list: return
_host_lobby_players_list.clear()
var nm = get_node("/root/NetworkManager")
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:
var p_ping = 0
if nm.player_stats.has(pid): p_ping = nm.player_stats[pid].get("ping", 0)
if pid == 1:
_host_lobby_players_list.add_item("%s (Host) - %sms" % [host_name, host_ping])
else:
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"):
+213
View File
@@ -0,0 +1,213 @@
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_and_offsets_preset(Control.PRESET_TOP_RIGHT)
kf_margin.grow_horizontal = Control.GROW_DIRECTION_BEGIN
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)
c_name.add_theme_color_override("font_color", Color(p_data.get("color", "cccccc")))
_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, v_color: String = "cccccc", k_color: String = "cccccc") -> void:
var l = RichTextLabel.new()
l.bbcode_enabled = true
l.fit_content = true
l.autowrap_mode = TextServer.AUTOWRAP_OFF
if killer == "":
l.text = "[color=#%s]%s[/color] died." % [v_color, victim]
else:
l.text = "[color=#%s]%s[/color] [%s] [color=#%s]%s[/color]" % [k_color, killer, weapon, v_color, victim]
l.add_theme_font_size_override("normal_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)
+1
View File
@@ -0,0 +1 @@
uid://dymuoha2tdj63
+6
View File
@@ -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")
+35 -4
View File
@@ -92,7 +92,21 @@ func _resume() -> void:
func _respawn() -> void: func _respawn() -> void:
_resume() # Unpause _resume() # Unpause
get_tree().reload_current_scene()
# Find local player
var local_player: Node3D = null
if get_tree().current_scene.has_node("Players"):
for p in get_tree().current_scene.get_node("Players").get_children():
if p.has_method("is_multiplayer_authority") and p.is_multiplayer_authority():
local_player = p
break
if not local_player:
local_player = get_tree().current_scene.get_node_or_null("Player")
if local_player and local_player.has_method("take_damage"):
local_player.take_damage(9999, Vector3.ZERO)
func _build_ui() -> void: func _build_ui() -> void:
bg = ColorRect.new() bg = ColorRect.new()
@@ -239,9 +253,26 @@ func _build_ui() -> void:
var general_tab = VBoxContainer.new() var general_tab = VBoxContainer.new()
general_tab.name = "General" general_tab.name = "General"
tab_container.add_child(general_tab) tab_container.add_child(general_tab)
var gen_label = Label.new() var user_hbox = HBoxContainer.new()
gen_label.text = "Settings coming soon..." general_tab.add_child(user_hbox)
general_tab.add_child(gen_label)
var user_lbl = Label.new()
user_lbl.text = "Username:"
user_lbl.custom_minimum_size = Vector2(150, 0)
user_hbox.add_child(user_lbl)
var user_input = LineEdit.new()
user_input.text = SettingsManager.username
user_input.custom_minimum_size = Vector2(200, 0)
user_hbox.add_child(user_input)
var user_save = Button.new()
user_save.text = "Save"
user_save.pressed.connect(func():
SettingsManager.username = user_input.text
SettingsManager._save_settings()
)
user_hbox.add_child(user_save)
var spacer_gen = Control.new() var spacer_gen = Control.new()
spacer_gen.size_flags_vertical = Control.SIZE_EXPAND_FILL spacer_gen.size_flags_vertical = Control.SIZE_EXPAND_FILL