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 = $WeaponsContainer @onready var enemies_container: Node3D = $EnemiesContainer @onready var menu: Control = $Menu @onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu @export var player_scene: PackedScene @export var practice_dummy_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 # Track if we've already initialized to prevent double-spawning var _multiplayer_initialized: bool = false # 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(): print("[Level] _ready() called. Peer ID: ", multiplayer.get_unique_id(), " Is server: ", multiplayer.is_server()) 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") # Create or find enemies container if has_node("EnemiesContainer"): enemies_container = get_node("EnemiesContainer") else: enemies_container = Node3D.new() enemies_container.name = "EnemiesContainer" add_child(enemies_container) print("Created EnemiesContainer") # Don't initialize weapons in _ready() - wait for Host/Join to be pressed # This is handled in initialize_multiplayer() which is called after the # multiplayer peer is properly set up print("[Level] _ready() complete - waiting for Host/Join") func _on_connected_to_server(): print("[Level] Connected to server! Cleaning up manual weapons") _cleanup_manual_weapons_on_client() ## Called by Network when multiplayer peer is set up func initialize_multiplayer(): print("[Level] initialize_multiplayer called. is_server: ", multiplayer.is_server()) # Prevent double initialization if _multiplayer_initialized: print("[Level] Already initialized, skipping") return _multiplayer_initialized = true if multiplayer.is_server(): print("[Level] Running server initialization") 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() # Spawn practice dummies _spawn_practice_dummies() # Spawn the host player (peer ID 1) print("[Level] Spawning host player") var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"}) _add_player(1, host_info) else: # Client initialization - clean up manual weapons immediately print("[Level] Running client initialization - cleaning up manual weapons") _cleanup_manual_weapons_on_client() func _cleanup_manual_weapons_on_client(): """Remove manually placed weapons on clients (server will sync them via RPC)""" print("[Client ", multiplayer.get_unique_id(), "] _cleanup_manual_weapons_on_client called") if not weapons_container: print("[Client] No weapons_container found!") return print("[Client] WeaponsContainer has ", weapons_container.get_child_count(), " children") var weapons_to_remove = [] for child in weapons_container.get_children(): print("[Client] Checking child: ", child.name, " (type: ", child.get_class(), ")") if child is WorldWeapon: print("[Client] - Is WorldWeapon with weapon_id: ", child.weapon_id) if child.weapon_id == -1: weapons_to_remove.append(child) print("[Client] - Marked for removal") print("[Client] Found ", weapons_to_remove.size(), " weapons to remove") for weapon in weapons_to_remove: print("[Client] Removing manually placed weapon: ", weapon.name) # Use immediate removal to prevent RPC errors weapons_container.remove_child(weapon) weapon.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): print("[Server] _on_player_connected called for peer ", peer_id, " with info: ", player_info) _add_player(peer_id, player_info) # Sync existing players to the newly joined player if multiplayer.is_server() and peer_id != 1: print("[Server] Syncing existing players to newly connected peer: ", peer_id) # Wait a frame to ensure new player is fully initialized await get_tree().process_frame for existing_player in players_container.get_children(): var existing_id = int(existing_player.name) if existing_id != peer_id: # Don't sync the player to themselves print("[Server] Syncing existing player ", existing_id, " to peer ", peer_id) rpc_id(peer_id, "_spawn_player_local", existing_id, existing_player.position) # 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 in _active_weapons: ", _active_weapons.keys()) 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, " (", weapon.weapon_data.weapon_name, ") at position ", weapon.global_position, " 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 ) else: print("[Server] Skipping invalid weapon ", weapon_id) # Sync existing enemies to the newly joined player print("[Server] Syncing enemies to newly connected peer: ", peer_id) if enemies_container: for enemy in enemies_container.get_children(): if enemy is BaseEnemy: # Extract ID from name (e.g., "PracticeDummy_1" -> 1) var enemy_name_parts = enemy.name.split("_") if enemy_name_parts.size() >= 2: var enemy_id = enemy_name_parts[-1].to_int() print("[Server] Syncing enemy ", enemy.name, " at position ", enemy.global_position, " to peer ", peer_id) rpc_id(peer_id, "_spawn_dummy_local", enemy_id, enemy.global_position) # Sync equipped weapons for all existing players to the newly joined player print("[Server] Syncing equipped weapons to newly connected peer: ", peer_id) for player_node in players_container.get_children(): var player = player_node as Character if player and is_instance_valid(player): # Skip the newly joined player (they don't have weapons yet) if int(player.name) == peer_id: continue # Sync main hand weapon if player.equipped_weapon and player.equipped_weapon.weapon_data: print("[Server] Syncing main hand weapon for player ", player.name, " to peer ", peer_id) player.rpc_id(peer_id, "equip_weapon_from_world", player.equipped_weapon.weapon_data.resource_path) # Sync off-hand weapon if player.equipped_offhand and player.equipped_offhand.weapon_data: print("[Server] Syncing off-hand weapon for player ", player.name, " to peer ", peer_id) player.rpc_id(peer_id, "equip_weapon_from_world", player.equipped_offhand.weapon_data.resource_path) func _on_host_pressed(): print("[Level] Host button pressed") menu.hide() print("[Level] Calling Network.start_host()") Network.start_host(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower()) print("[Level] Waiting one frame...") await get_tree().process_frame print("[Level] Calling initialize_multiplayer()") initialize_multiplayer() func _on_join_pressed(): print("[Level] Join button pressed") menu.hide() print("[Level] Calling Network.join_game()") Network.join_game(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower(), address_input.text.strip_edges()) print("[Level] Waiting one frame...") await get_tree().process_frame print("[Level] Calling initialize_multiplayer()") initialize_multiplayer() func _add_player(id: int, player_info : Dictionary): print("[Level] _add_player called for peer ", id, " with info: ", player_info) # Server spawns player and replicates to all clients via RPC if multiplayer.is_server(): if players_container.has_node(str(id)): print("[Level] Player ", id, " already exists, skipping") return var spawn_pos = get_spawn_point() print("[Level] Server spawning player ", id, " at ", spawn_pos) # Spawn on server and all clients (call_local does both) rpc("_spawn_player_local", id, spawn_pos) @rpc("any_peer", "call_local", "reliable") func _spawn_player_local(id: int, spawn_pos: Vector3): if players_container.has_node(str(id)): print("[Peer ", multiplayer.get_unique_id(), "] Player ", id, " already exists, skipping") return print("[Peer ", multiplayer.get_unique_id(), "] Creating player instance for peer ", id) var player = player_scene.instantiate() player.name = str(id) player.position = spawn_pos print("[Peer ", multiplayer.get_unique_id(), "] Adding player to PlayersContainer") players_container.add_child(player, true) print("[Peer ", multiplayer.get_unique_id(), "] Player ", id, " spawned at ", player.position) # Get player info from Network var player_info = Network.players.get(id, {"nick": "Player", "skin": "blue"}) var nick = player_info["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" # ---------- ENEMY SPAWNING ---------- func _spawn_practice_dummies(): if not multiplayer.is_server(): return if not practice_dummy_scene: push_warning("Practice dummy scene not assigned!") return # Wait a frame for everything to be ready await get_tree().process_frame print("[Server] Spawning practice dummies") # Spawn dummies at different positions var dummy_positions = [ Vector3(10, 0, 0), Vector3(-10, 0, 0), Vector3(0, 0, 10), Vector3(0, 0, -10), ] var dummy_counter = 0 for pos in dummy_positions: dummy_counter += 1 rpc("_spawn_dummy_local", dummy_counter, pos) ## Spawn a practice dummy on all clients @rpc("any_peer", "call_local", "reliable") func _spawn_dummy_local(dummy_id: int, spawn_pos: Vector3): if not practice_dummy_scene: push_error("Practice dummy scene not loaded!") return if not enemies_container: push_error("EnemiesContainer not found!") return var dummy_name = "PracticeDummy_" + str(dummy_id) # Don't spawn duplicates if enemies_container.has_node(dummy_name): print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " already exists") return print("[Peer ", multiplayer.get_unique_id(), "] Spawning dummy ", dummy_name, " at ", spawn_pos) var dummy = practice_dummy_scene.instantiate() dummy.name = dummy_name dummy.position = spawn_pos # Set multiplayer authority to server dummy.set_multiplayer_authority(1) enemies_container.add_child(dummy, true) print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " spawned successfully") # ---------- 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 # Immediately remove from active weapons to prevent late-join sync issues if _active_weapons.has(weapon_id): _active_weapons.erase(weapon_id) print("[Server] Removed weapon ", weapon_id, " from _active_weapons. Remaining: ", _active_weapons.size()) print("[Server] Remaining weapon IDs: ", _active_weapons.keys()) else: print("[Server] WARNING: Weapon ", weapon_id, " not found in _active_weapons!") # 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)