extends Node3D const MAX_CHAT_MESSAGE_LENGTH := 160 const MIN_NICKNAME_HEIGHT := 2.0 const MAX_NICKNAME_HEIGHT := 8.0 @export var player_scene: PackedScene var chat_visible := false var inventory_visible := false var player_list_visible := false var _player_nickname_heights: Dictionary = {} var _nickname_heights_requested := false var _nickname_height_requesters: Dictionary = {} @onready var players_container: Node3D = $PlayersContainer @onready var main_menu: MainMenuUI = $MainMenuUI @onready var multiplayer_chat: MultiplayerChatUI = $MultiplayerChatUI @onready var inventory_ui: InventoryUI = $InventoryUI @onready var player_list_ui: PlayerListUI = $PlayerListUI @onready var pause_menu: PauseMenuUI = $PauseMenuUI func _ready(): after_ready() if DisplayServer.get_name() == "headless": Network.start_host("", "") pause_menu.hide_menu() main_menu.show_menu() multiplayer_chat.set_process_input(true) main_menu.host_pressed.connect(_on_host_pressed) main_menu.join_pressed.connect(_on_join_pressed) main_menu.quit_pressed.connect(_on_quit_pressed) pause_menu.resume_pressed.connect(_on_pause_resume_pressed) pause_menu.main_menu_pressed.connect(_on_pause_main_menu_pressed) pause_menu.quit_pressed.connect(_on_pause_quit_pressed) if inventory_ui: inventory_ui.inventory_closed.connect(_on_inventory_closed) if multiplayer_chat: multiplayer_chat.message_sent.connect(_on_chat_message_sent) Network.server_disconnected.connect(_on_server_disconnected) Network.connect("player_connected", Callable(self, "_on_player_connected")) multiplayer.peer_disconnected.connect(_remove_player) _update_mouse_mode() func _process(_delta: float) -> void: var can_show_player_list := ( not main_menu.is_menu_visible() and not pause_menu.is_menu_visible() and multiplayer.has_multiplayer_peer() ) if Input.is_key_pressed(KEY_TAB) and can_show_player_list: if not player_list_visible: _show_player_list() elif player_list_visible: _hide_player_list() func after_ready(): var ip_address: String if OS.has_feature("windows"): if OS.has_environment("COMPUTERNAME"): ip_address = IP.resolve_hostname(str(OS.get_environment("COMPUTERNAME")), IP.TYPE_IPV4) elif OS.has_feature("x11"): if OS.has_environment("HOSTNAME"): ip_address = IP.resolve_hostname(str(OS.get_environment("HOSTNAME")), IP.TYPE_IPV4) elif OS.has_feature("OSX"): if OS.has_environment("HOSTNAME"): ip_address = IP.resolve_hostname(str(OS.get_environment("HOSTNAME")), IP.TYPE_IPV4) main_menu.address_input.text = ip_address func _on_server_disconnected(): _reset_session_ui() func _reset_session_ui() -> void: for child in players_container.get_children(): child.queue_free() var loot_root := get_node_or_null("Environment/ItemContainer") if loot_root: for child in loot_root.get_children(): child.queue_free() chat_visible = false inventory_visible = false _hide_player_list() _player_nickname_heights.clear() _nickname_heights_requested = false _nickname_height_requesters.clear() multiplayer_chat.close_chat() multiplayer_chat.clear_chat() if inventory_ui: inventory_ui.close_inventory() inventory_ui.current_player = null _hide_pause_menu(false) main_menu.show_menu() _update_mouse_mode() func _on_player_connected(peer_id, player_info): var player = _add_player(peer_id, player_info) if multiplayer.is_server() and player: player.call_deferred("_sync_inventory_to_owner") if multiplayer.is_server(): if peer_id != 1: call_deferred("_sync_nickname_heights_to_peer", peer_id) elif not _nickname_heights_requested: _nickname_heights_requested = true call_deferred("_request_nickname_heights") _refresh_player_list() func _on_host_pressed(nickname: String, skin: String): var error = Network.start_host(nickname, skin) if error: push_warning("Failed to host game. Error: " + str(error)) main_menu.show_menu() _update_mouse_mode() return main_menu.hide_menu() _update_mouse_mode() func _on_join_pressed(nickname: String, skin: String, address: String): var error = Network.join_game(nickname, skin, address) if error: push_warning("Failed to join game. Error: " + str(error)) main_menu.show_menu() _update_mouse_mode() return main_menu.hide_menu() _update_mouse_mode() func _add_player(id: int, player_info: Dictionary) -> Character: if DisplayServer.get_name() == "headless" and id == 1: return null if players_container.has_node(str(id)): return players_container.get_node(str(id)) as Character var player = player_scene.instantiate() player.name = str(id) player.position = get_spawn_point(id) players_container.add_child(player, true) var nick = Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(id)) player.nickname.text = nick var skin_enum = Network.sanitize_skin_value(player_info.get("skin", Character.SkinColor.BLUE)) player.set_player_skin(skin_enum) _apply_player_nickname_height(id) return player func get_spawn_point(id: int) -> Vector3: var spawn_angle := fmod(float(id) * 2.399963229728653, 2.0 * PI) var spawn_point := Vector2.from_angle(spawn_angle) * 10 return Vector3(spawn_point.x, 0, spawn_point.y) func _remove_player(id): _player_nickname_heights.erase(id) if not players_container.has_node(str(id)): call_deferred("_refresh_player_list") return var player_node = players_container.get_node(str(id)) if player_node: player_node.queue_free() call_deferred("_refresh_player_list") func _on_quit_pressed() -> void: Network.leave_game() get_tree().quit() func toggle_chat(): if main_menu.is_menu_visible() or is_gameplay_input_blocked(): return multiplayer_chat.toggle_chat() chat_visible = multiplayer_chat.is_chat_visible() _update_mouse_mode() func is_chat_visible() -> bool: return multiplayer_chat.is_chat_visible() func _input(event): if event.is_action_pressed("pause"): _handle_pause_action() get_viewport().set_input_as_handled() return if is_gameplay_input_blocked(): return if event.is_action_pressed("toggle_chat"): toggle_chat() elif chat_visible and multiplayer_chat.message.has_focus(): pass elif event.is_action_pressed("inventory"): toggle_inventory() elif event is InputEventKey and event.pressed and event.keycode == KEY_F1: _debug_add_item() elif event is InputEventKey and event.pressed and event.keycode == KEY_F2: _debug_print_inventory() func _on_chat_message_sent(message_text: String) -> void: chat_visible = multiplayer_chat.is_chat_visible() _update_mouse_mode() var trimmed_message = _sanitize_chat_message(message_text) if trimmed_message.is_empty(): return if multiplayer.is_server(): _broadcast_chat_message(multiplayer.get_unique_id(), trimmed_message) else: submit_chat_message.rpc_id(1, trimmed_message) @rpc("any_peer", "reliable") func submit_chat_message(message_text: String): if not multiplayer.is_server(): return var sender_id = multiplayer.get_remote_sender_id() var trimmed_message = _sanitize_chat_message(message_text) if trimmed_message.is_empty(): return _broadcast_chat_message(sender_id, trimmed_message) func _broadcast_chat_message(sender_id: int, message_text: String): var player_info = Network.players.get(sender_id, {}) var nick = Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(sender_id)) show_chat_message.rpc(nick, message_text) @rpc("authority", "call_local", "reliable") func show_chat_message(nick: String, msg: String): multiplayer_chat.add_message(nick, msg) func _sanitize_chat_message(message_text: String) -> String: var clean = message_text.strip_edges() if clean.length() > MAX_CHAT_MESSAGE_LENGTH: clean = clean.substr(0, MAX_CHAT_MESSAGE_LENGTH) return clean func toggle_inventory(): if main_menu.is_menu_visible() or is_gameplay_input_blocked(): return var local_player = _get_local_player() if not local_player: return inventory_visible = !inventory_visible if inventory_visible: inventory_ui.open_inventory(local_player) else: inventory_ui.close_inventory() _update_mouse_mode() func is_inventory_visible() -> bool: return inventory_visible func _show_player_list() -> void: if main_menu.is_menu_visible() or pause_menu.is_menu_visible() or not multiplayer.has_multiplayer_peer(): return player_list_visible = true player_list_ui.show_players(Network.players, multiplayer.get_unique_id()) func _hide_player_list() -> void: player_list_visible = false if player_list_ui: player_list_ui.hide_players() func _refresh_player_list() -> void: if player_list_visible and player_list_ui: player_list_ui.refresh_players(Network.players, multiplayer.get_unique_id()) func register_player_nickname_height(player_id: int, height: float) -> void: if not multiplayer.is_server() or player_id <= 0: return var normalized_height := _normalize_nickname_height(height) _store_and_apply_player_nickname_height(player_id, normalized_height) sync_player_nickname_height.rpc(player_id, normalized_height) @rpc("authority", "reliable") func sync_player_nickname_height(player_id: int, height: float) -> void: if multiplayer.get_remote_sender_id() != 1: return _store_and_apply_player_nickname_height(player_id, _normalize_nickname_height(height)) func _request_nickname_heights() -> void: if multiplayer.is_server() or not multiplayer.has_multiplayer_peer(): return request_nickname_heights.rpc_id(1) @rpc("any_peer", "reliable") func request_nickname_heights() -> void: if not multiplayer.is_server(): return var requester_id := multiplayer.get_remote_sender_id() if requester_id <= 0 or _nickname_height_requesters.has(requester_id): return _nickname_height_requesters[requester_id] = true _sync_nickname_heights_to_peer(requester_id) func _sync_nickname_heights_to_peer(peer_id: int) -> void: if not multiplayer.is_server() or peer_id <= 0: return for child in players_container.get_children(): var player := child as Character if not player: continue var player_id := str(player.name).to_int() var height := float(_player_nickname_heights.get(player_id, player.get_current_nickname_height())) sync_player_nickname_height.rpc_id(peer_id, player_id, _normalize_nickname_height(height)) func _store_and_apply_player_nickname_height(player_id: int, height: float) -> void: if player_id <= 0: return _player_nickname_heights[player_id] = height _apply_player_nickname_height(player_id) func _apply_player_nickname_height(player_id: int) -> void: if not _player_nickname_heights.has(player_id): return var player := players_container.get_node_or_null(str(player_id)) as Character if player: player.apply_synced_nickname_height(float(_player_nickname_heights[player_id])) func _normalize_nickname_height(height: float) -> float: if is_nan(height) or is_inf(height): return MIN_NICKNAME_HEIGHT return clampf(height, MIN_NICKNAME_HEIGHT, MAX_NICKNAME_HEIGHT) func _on_inventory_closed(): inventory_visible = false _update_mouse_mode() func is_gameplay_input_blocked() -> bool: return pause_menu.is_menu_visible() func is_camera_input_blocked() -> bool: return is_gameplay_input_blocked() or inventory_visible or multiplayer_chat.is_chat_visible() func _handle_pause_action() -> void: if inventory_visible: _close_inventory() return if multiplayer_chat.is_chat_visible(): _close_chat() return if pause_menu.is_menu_visible(): _hide_pause_menu() return if main_menu.is_menu_visible() or not multiplayer.has_multiplayer_peer(): return _show_pause_menu() func _show_pause_menu() -> void: pause_menu.show_menu() _update_mouse_mode() func _hide_pause_menu(restore_mouse_mode: bool = true) -> void: if not pause_menu.is_menu_visible(): return pause_menu.hide_menu() if restore_mouse_mode: _update_mouse_mode() func _close_chat() -> void: multiplayer_chat.close_chat() chat_visible = false _update_mouse_mode() func _close_inventory() -> void: inventory_ui.close_inventory() inventory_visible = false _update_mouse_mode() func _update_mouse_mode() -> void: if DisplayServer.get_name() == "headless": return var ui_requires_cursor := ( main_menu.is_menu_visible() or pause_menu.is_menu_visible() or inventory_visible or not multiplayer.has_multiplayer_peer() ) if ui_requires_cursor: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE else: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func _on_pause_resume_pressed() -> void: _hide_pause_menu() func _on_pause_main_menu_pressed() -> void: _hide_pause_menu(false) Network.leave_game() if not main_menu.is_menu_visible(): _reset_session_ui() func _on_pause_quit_pressed() -> void: Network.leave_game() get_tree().quit() func update_local_inventory_display(): if inventory_ui: inventory_ui.refresh_display() func _get_local_player() -> Character: var local_player_id = multiplayer.get_unique_id() if players_container.has_node(str(local_player_id)): return players_container.get_node(str(local_player_id)) as Character return null func _debug_add_item(): if not OS.is_debug_build() or not multiplayer.is_server(): return var local_player = _get_local_player() if local_player: var test_items: Array[String] = [] for item_id in ItemDatabase.get_all_items(): test_items.append(str(item_id)) if test_items.is_empty(): return var random_item = test_items[randi() % test_items.size()] local_player.request_add_item.rpc_id(1, random_item, 1) func _debug_print_inventory(): var local_player = _get_local_player() if local_player and local_player.get_inventory(): var inventory = local_player.get_inventory() print("=== Inventory Debug ===") for i in range(inventory.slots.size()): var slot = inventory.get_slot(i) if slot and not slot.is_empty(): print("Slot ", i, ": ", slot.item_id, " x", slot.quantity) print("=====================") else: print("No inventory found for local player")