You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
399 lines
14 KiB
399 lines
14 KiB
extends Node3D |
|
|
|
@onready var skin_input: LineEdit = $Menu/MainContainer/MainMenu/Option2/SkinInput |
|
@onready var nick_input: LineEdit = $Menu/MainContainer/MainMenu/Option1/NickInput |
|
@onready var address_input: LineEdit = $Menu/MainContainer/MainMenu/Option3/AddressInput |
|
@onready var players_container: Node3D = $PlayersContainer |
|
@onready var weapons_container: Node3D = null # Will be created if doesn't exist |
|
@onready var menu: Control = $Menu |
|
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu |
|
@export var player_scene: PackedScene |
|
|
|
# Weapon spawning counter (server-side only) |
|
var _weapon_spawn_counter: int = 0 |
|
# Track active weapons for late-join sync (server-side only) |
|
var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference |
|
|
|
# multiplayer chat |
|
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message |
|
@onready var send: Button = $MultiplayerChat/VBoxContainer/HBoxContainer/Send |
|
@onready var chat: TextEdit = $MultiplayerChat/VBoxContainer/Chat |
|
@onready var multiplayer_chat: Control = $MultiplayerChat |
|
|
|
var chat_visible = false |
|
|
|
func _ready(): |
|
multiplayer_chat.hide() |
|
menu.show() |
|
multiplayer_chat.set_process_input(true) |
|
|
|
# Add quick-fill preset buttons |
|
_create_preset_buttons() |
|
|
|
# Create or find weapons container |
|
if has_node("WeaponsContainer"): |
|
weapons_container = get_node("WeaponsContainer") |
|
else: |
|
weapons_container = Node3D.new() |
|
weapons_container.name = "WeaponsContainer" |
|
add_child(weapons_container) |
|
print("Created WeaponsContainer") |
|
|
|
# Clients: Remove manually placed weapons (server will sync them via RPC) |
|
if not multiplayer.is_server(): |
|
_cleanup_manual_weapons_on_client() |
|
return |
|
|
|
Network.connect("player_connected", Callable(self, "_on_player_connected")) |
|
multiplayer.peer_disconnected.connect(_remove_player) |
|
|
|
# Initialize any manually placed weapons in the scene |
|
_initialize_manual_weapons() |
|
|
|
# Spawn initial weapons when server starts |
|
_spawn_initial_weapons() |
|
|
|
func _cleanup_manual_weapons_on_client(): |
|
"""Remove manually placed weapons on clients (server will sync them via RPC)""" |
|
if not weapons_container: |
|
return |
|
|
|
var weapons_to_remove = [] |
|
for child in weapons_container.get_children(): |
|
if child is WorldWeapon and child.weapon_id == -1: |
|
weapons_to_remove.append(child) |
|
|
|
for weapon in weapons_to_remove: |
|
print("[Client] Removing manually placed weapon: ", weapon.name) |
|
weapon.queue_free() |
|
|
|
func _initialize_manual_weapons(): |
|
"""Initialize any WorldWeapon nodes manually placed in the level scene""" |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
if not weapons_container: |
|
return |
|
|
|
# Find all WorldWeapon nodes in the weapons container |
|
var manual_weapons = [] |
|
for child in weapons_container.get_children(): |
|
if child is WorldWeapon: |
|
manual_weapons.append(child) |
|
|
|
if manual_weapons.is_empty(): |
|
print("[Server] No manually placed weapons found") |
|
return |
|
|
|
print("[Server] Found ", manual_weapons.size(), " manually placed weapon(s)") |
|
|
|
# Initialize each manually placed weapon |
|
for weapon in manual_weapons: |
|
# Skip if already initialized (weapon_id != -1) |
|
if weapon.weapon_id != -1: |
|
continue |
|
|
|
# Assign unique ID |
|
_weapon_spawn_counter += 1 |
|
weapon.weapon_id = _weapon_spawn_counter |
|
|
|
# Set deterministic name for networking |
|
var old_name = weapon.name |
|
weapon.name = "WorldWeapon_" + str(weapon.weapon_id) |
|
|
|
# Track in active weapons |
|
_active_weapons[weapon.weapon_id] = weapon |
|
|
|
# Connect cleanup signal |
|
weapon.tree_exiting.connect(_on_weapon_removed.bind(weapon.weapon_id)) |
|
|
|
print("[Server] Initialized manual weapon '", old_name, "' with ID: ", weapon.weapon_id, " at position: ", weapon.global_position) |
|
|
|
# Verify weapon_data is set |
|
if not weapon.weapon_data: |
|
push_error("Manual weapon '", old_name, "' has no WeaponData assigned!") |
|
continue |
|
|
|
func _spawn_initial_weapons(): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
# Wait a frame for everything to be ready |
|
await get_tree().process_frame |
|
|
|
print("[Server] _spawn_initial_weapons - Connected peers: ", multiplayer.get_peers()) |
|
|
|
# Spawn a sword |
|
_weapon_spawn_counter += 1 |
|
print("[Server] Calling RPC to spawn sword with ID: ", _weapon_spawn_counter) |
|
rpc("spawn_world_weapon", |
|
"res://level/resources/weapon_sword.tres", |
|
Vector3(5, 1, 0), |
|
Vector3.ZERO, |
|
_weapon_spawn_counter |
|
) |
|
|
|
# Spawn a shield |
|
_weapon_spawn_counter += 1 |
|
print("[Server] Calling RPC to spawn shield with ID: ", _weapon_spawn_counter) |
|
rpc("spawn_world_weapon", |
|
"res://level/resources/weapon_shield.tres", |
|
Vector3(-5, 1, 0), |
|
Vector3.ZERO, |
|
_weapon_spawn_counter |
|
) |
|
|
|
func _on_player_connected(peer_id, player_info): |
|
_add_player(peer_id, player_info) |
|
|
|
# Sync existing weapons to the newly joined player (but not to server itself) |
|
if multiplayer.is_server() and peer_id != 1: |
|
print("[Server] Syncing weapons to newly connected peer: ", peer_id) |
|
print("[Server] Active weapons count: ", _active_weapons.size()) |
|
for weapon_id in _active_weapons.keys(): |
|
var weapon = _active_weapons[weapon_id] |
|
if is_instance_valid(weapon) and weapon.weapon_data: |
|
print("[Server] Sending weapon ", weapon_id, " to peer ", peer_id) |
|
# Send current position and zero velocity for syncing |
|
rpc_id(peer_id, "_client_spawn_weapon", |
|
weapon.weapon_data.resource_path, |
|
weapon.global_position, |
|
Vector3.ZERO, |
|
weapon_id |
|
) |
|
|
|
func _on_host_pressed(): |
|
menu.hide() |
|
Network.start_host(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower()) |
|
|
|
func _on_join_pressed(): |
|
menu.hide() |
|
Network.join_game(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower(), address_input.text.strip_edges()) |
|
|
|
func _add_player(id: int, player_info : Dictionary): |
|
if players_container.has_node(str(id)): |
|
return |
|
var player = player_scene.instantiate() |
|
player.name = str(id) |
|
player.position = get_spawn_point() |
|
players_container.add_child(player, true) |
|
|
|
var nick = Network.players[id]["nick"] |
|
# Access nickname directly via node path since @onready hasn't loaded yet |
|
var nickname_label = player.get_node_or_null("PlayerNick/Nickname") |
|
if nickname_label: |
|
nickname_label.text = nick |
|
# player.rpc("change_nick", nick) |
|
|
|
# Set up HUD for local player |
|
if id == multiplayer.get_unique_id(): |
|
print("[Level] Setting up HUD for local player") |
|
# Wait a frame to ensure HUD autoload is ready |
|
await get_tree().process_frame |
|
if has_node("/root/HUD"): |
|
get_node("/root/HUD").set_local_player(player) |
|
else: |
|
push_warning("[Level] HUD autoload not found! Make sure to restart Godot to register the new autoload.") |
|
|
|
var skin_enum = player_info["skin"] |
|
player.set_player_skin(skin_enum) |
|
# rpc("sync_player_skin", id, skin_enum) |
|
|
|
# rpc("sync_player_position", id, player.position) |
|
|
|
func get_spawn_point() -> Vector3: |
|
var spawn_point = Vector2.from_angle(randf() * 2 * PI) * 10 # spawn radius |
|
return Vector3(spawn_point.x, 0, spawn_point.y) |
|
|
|
func _remove_player(id): |
|
if not multiplayer.is_server() or not players_container.has_node(str(id)): |
|
return |
|
var player_node = players_container.get_node(str(id)) |
|
if player_node: |
|
player_node.queue_free() |
|
|
|
# @rpc("any_peer", "call_local") |
|
# func sync_player_position(id: int, new_position: Vector3): |
|
# var player = players_container.get_node(str(id)) |
|
# if player: |
|
# player.position = new_position |
|
|
|
# @rpc("any_peer", "call_local") |
|
# func sync_player_skin(id: int, skin_color: Character.SkinColor): |
|
# var player = players_container.get_node(str(id)) |
|
# if player: |
|
# player.set_player_skin(skin_color) |
|
|
|
func _on_quit_pressed() -> void: |
|
get_tree().quit() |
|
|
|
# ---------- MULTIPLAYER CHAT ---------- |
|
func toggle_chat(): |
|
if menu.visible: |
|
return |
|
|
|
chat_visible = !chat_visible |
|
if chat_visible: |
|
multiplayer_chat.show() |
|
message.grab_focus() |
|
else: |
|
multiplayer_chat.hide() |
|
get_viewport().set_input_as_handled() |
|
|
|
func is_chat_visible() -> bool: |
|
return chat_visible |
|
|
|
func _input(event): |
|
if event.is_action_pressed("toggle_chat"): |
|
toggle_chat() |
|
elif event is InputEventKey and event.keycode == KEY_ENTER: |
|
_on_send_pressed() |
|
|
|
func _on_send_pressed() -> void: |
|
var trimmed_message = message.text.strip_edges() |
|
if trimmed_message == "": |
|
return # do not send empty messages |
|
|
|
var nick = Network.players[multiplayer.get_unique_id()]["nick"] |
|
|
|
rpc("msg_rpc", nick, trimmed_message) |
|
message.text = "" |
|
message.grab_focus() |
|
|
|
@rpc("any_peer", "call_local") |
|
func msg_rpc(nick, msg): |
|
chat.text += str(nick, " : ", msg, "\n") |
|
|
|
# ---------- PRESET BUTTONS ---------- |
|
func _create_preset_buttons(): |
|
# Create a container for preset buttons |
|
var preset_container = HBoxContainer.new() |
|
preset_container.name = "PresetContainer" |
|
|
|
var preset_label = Label.new() |
|
preset_label.text = "Quick Fill:" |
|
preset_container.add_child(preset_label) |
|
|
|
# Scott button |
|
var scott_button = Button.new() |
|
scott_button.text = "Scott" |
|
scott_button.pressed.connect(_on_scott_preset) |
|
preset_container.add_child(scott_button) |
|
|
|
# Jemz button |
|
var jemz_button = Button.new() |
|
jemz_button.text = "Jemz" |
|
jemz_button.pressed.connect(_on_jemz_preset) |
|
preset_container.add_child(jemz_button) |
|
|
|
# Add to main menu at the top |
|
main_menu.add_child(preset_container) |
|
main_menu.move_child(preset_container, 0) |
|
|
|
func _on_scott_preset(): |
|
nick_input.text = "Scott" |
|
skin_input.text = "Blue" |
|
address_input.text = "127.0.0.1" |
|
|
|
func _on_jemz_preset(): |
|
nick_input.text = "Jemz" |
|
skin_input.text = "Red" |
|
address_input.text = "127.0.0.1" |
|
|
|
# ---------- WEAPON SPAWNING ---------- |
|
## Spawn a weapon in the world (called from server, syncs to all clients) |
|
@rpc("any_peer", "call_local", "reliable") |
|
func spawn_world_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int): |
|
print("[Client ", multiplayer.get_unique_id(), "] spawn_world_weapon called for weapon_id: ", weapon_id) |
|
|
|
if not weapons_container: |
|
push_error("WeaponsContainer not found in level!") |
|
return |
|
|
|
# Load the weapon data resource |
|
var weapon_data = load(weapon_data_path) as WeaponData |
|
if not weapon_data: |
|
push_error("Failed to load weapon data from: " + weapon_data_path) |
|
return |
|
|
|
# Create WorldWeapon instance |
|
var world_weapon = WorldWeapon.new() |
|
world_weapon.weapon_data = weapon_data |
|
print("[DEBUG] About to set weapon_id. Parameter weapon_id = ", weapon_id) |
|
world_weapon.weapon_id = weapon_id # Store the ID |
|
print("[DEBUG] After setting weapon_id. world_weapon.weapon_id = ", world_weapon.weapon_id) |
|
world_weapon.name = "WorldWeapon_" + str(weapon_id) # Deterministic name |
|
print("[DEBUG] Set weapon name to: ", world_weapon.name, " using weapon_id: ", weapon_id) |
|
world_weapon.position = spawn_position |
|
|
|
# Remove existing weapon with same name if it exists (prevents duplicates) |
|
var weapon_path = NodePath(world_weapon.name) |
|
if weapons_container.has_node(weapon_path): |
|
print("[DEBUG] Weapon ", world_weapon.name, " already exists, removing old one first") |
|
var old_weapon = weapons_container.get_node(weapon_path) |
|
weapons_container.remove_child(old_weapon) |
|
old_weapon.queue_free() |
|
|
|
# Add to weapons container |
|
weapons_container.add_child(world_weapon) |
|
print("[DEBUG] After add_child, weapon name in tree: ", world_weapon.name, " weapon_id: ", world_weapon.weapon_id) |
|
|
|
# Track this weapon on the server (AFTER adding to tree so signals work) |
|
if multiplayer.is_server(): |
|
_active_weapons[weapon_id] = world_weapon |
|
print("[Server] Added weapon ", weapon_id, " to _active_weapons. Total: ", _active_weapons.size()) |
|
# Connect to the weapon's removal signal |
|
world_weapon.tree_exiting.connect(_on_weapon_removed.bind(weapon_id)) |
|
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Spawned weapon: ", world_weapon.name, " at ", spawn_position) |
|
|
|
# Apply velocity after physics is ready |
|
if initial_velocity != Vector3.ZERO: |
|
await get_tree().process_frame |
|
world_weapon.linear_velocity = initial_velocity |
|
|
|
## Remove a world weapon from all clients (called by WorldWeapon when picked up) |
|
func remove_world_weapon(weapon_id: int): |
|
print("[Server] remove_world_weapon called for weapon_id: ", weapon_id) |
|
if not multiplayer.is_server(): |
|
print("[ERROR] remove_world_weapon called on client!") |
|
return |
|
|
|
# Broadcast removal to all clients |
|
print("[Server] Broadcasting removal RPC to all clients") |
|
rpc("_remove_weapon_on_clients", weapon_id) |
|
|
|
## RPC to remove weapon on all clients |
|
@rpc("any_peer", "call_local", "reliable") |
|
func _remove_weapon_on_clients(weapon_id: int): |
|
print("[Peer ", multiplayer.get_unique_id(), "] _remove_weapon_on_clients called for weapon_id: ", weapon_id) |
|
var weapon_name = "WorldWeapon_" + str(weapon_id) |
|
if weapons_container and weapons_container.has_node(weapon_name): |
|
var weapon = weapons_container.get_node(weapon_name) |
|
print("[Peer ", multiplayer.get_unique_id(), "] Removing weapon ", weapon_name) |
|
weapon.queue_free() |
|
else: |
|
print("[Peer ", multiplayer.get_unique_id(), "] WARNING: Weapon ", weapon_name, " not found in WeaponsContainer") |
|
|
|
## Called when a weapon is removed (picked up or destroyed) |
|
func _on_weapon_removed(weapon_id: int): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
# Remove from tracking |
|
if _active_weapons.has(weapon_id): |
|
_active_weapons.erase(weapon_id) |
|
print("Removed weapon ", weapon_id, " from active tracking") |
|
|
|
## Client-only spawn (called via rpc_id for late-join sync) |
|
@rpc("any_peer", "reliable") |
|
func _client_spawn_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int): |
|
print("[Client ", multiplayer.get_unique_id(), "] _client_spawn_weapon received for weapon_id: ", weapon_id) |
|
# This only runs on clients, not server (no call_local) |
|
if multiplayer.is_server(): |
|
print("[ERROR] _client_spawn_weapon called on server!") |
|
return |
|
|
|
# Call the regular spawn function to create the weapon |
|
print("[Client ", multiplayer.get_unique_id(), "] Calling spawn_world_weapon locally") |
|
spawn_world_weapon(weapon_data_path, spawn_position, initial_velocity, weapon_id)
|
|
|