extends Node # ============================================================================ # MapMultiplayerController.gd - 地图远端玩家同步 # ============================================================================ # 负责把本地玩家位置发送给聊天 WebSocket,并把后端位置广播渲染为远端玩家。 # ============================================================================ const REMOTE_PLAYER_SCENE: PackedScene = preload("res://scenes/characters/remote_player.tscn") const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn") const DEFAULT_MAP_ID: String = "whale_port" const POSITION_SEND_INTERVAL: float = 0.12 const MIN_POSITION_DELTA: float = 4.0 const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer" const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84) const PRIVATE_CHAT_INTERACTION_DISTANCE: float = 150.0 const PRIVATE_CHAT_FORWARD_DOT_THRESHOLD: float = 0.25 @export var map_id: String = DEFAULT_MAP_ID @export var local_player_path: NodePath = NodePath("../Characters/Players/Player") @export var remote_players_root_path: NodePath = NodePath("../Characters/Players/RemotePlayers") var _local_player: Node2D var _remote_players_root: Node2D var _remote_players: Dictionary = {} var _pending_remote_players: Dictionary = {} var _last_sent_position: Vector2 = Vector2.INF var _last_sent_at_msec: int = 0 var _last_private_chat_interaction_msec: int = 0 var _last_friend_request_interaction_msec: int = 0 func _ready() -> void: _local_player = get_node_or_null(local_player_path) as Node2D _remote_players_root = get_node_or_null(remote_players_root_path) as Node2D if _local_player == null: push_warning("MapMultiplayerController: local player not found.") if _remote_players_root == null: push_warning("MapMultiplayerController: remote players root not found.") var eventSystem := _get_event_system() if eventSystem == null: push_warning("MapMultiplayerController: EventSystem autoload is not available.") return eventSystem.call("connect_event", EventNames.PLAYER_MOVED, _on_local_player_moved, self) eventSystem.call("connect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self) eventSystem.call("connect_event", EventNames.REMOTE_PLAYERS_SNAPSHOT_READY, _on_remote_players_snapshot_ready, self) eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_POSITION_UPDATED, _on_remote_player_position_updated, self) eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_JOINED, _on_remote_player_joined, self) eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_LEFT, _on_remote_player_left, self) eventSystem.call("connect_event", EventNames.REMOTE_SKIN_READY, _on_remote_skin_ready, self) eventSystem.call("connect_event", EventNames.REMOTE_SKIN_FAILED, _on_remote_skin_failed, self) eventSystem.call("connect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self) eventSystem.call("connect_event", EventNames.INTERACT_PRESSED, _on_interact_pressed, self) call_deferred("_send_world_ready") func _process(_delta: float) -> void: if Input.is_action_just_pressed("interact"): _try_start_private_chat() if Input.is_action_just_pressed("friend_request"): _try_request_friend() func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("interact"): if _try_start_private_chat(): get_viewport().set_input_as_handled() return if event.is_action_pressed("friend_request") and _try_request_friend(): get_viewport().set_input_as_handled() func _exit_tree() -> void: var eventSystem := _get_event_system() if eventSystem == null: return eventSystem.call("disconnect_event", EventNames.PLAYER_MOVED, _on_local_player_moved, self) eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self) eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYERS_SNAPSHOT_READY, _on_remote_players_snapshot_ready, self) eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_POSITION_UPDATED, _on_remote_player_position_updated, self) eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_JOINED, _on_remote_player_joined, self) eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_LEFT, _on_remote_player_left, self) eventSystem.call("disconnect_event", EventNames.REMOTE_SKIN_READY, _on_remote_skin_ready, self) eventSystem.call("disconnect_event", EventNames.REMOTE_SKIN_FAILED, _on_remote_skin_failed, self) eventSystem.call("disconnect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self) eventSystem.call("disconnect_event", EventNames.INTERACT_PRESSED, _on_interact_pressed, self) func _get_event_system() -> Node: return get_node_or_null("/root/EventSystem") func _get_chat_manager() -> Node: return get_node_or_null("/root/ChatManager") func _get_settings_manager() -> Node: return get_node_or_null("/root/SettingsManager") func _find_chat_ui() -> Control: var current: Node = self while current != null: var chat_ui := current.get_node_or_null("UILayer/ChatUI") as Control if chat_ui != null: return chat_ui current = current.get_parent() return null func _is_text_input_focused() -> bool: var focusOwner: Control = get_viewport().gui_get_focus_owner() if focusOwner == null or not focusOwner.is_inside_tree() or not focusOwner.is_visible_in_tree(): return false if focusOwner is LineEdit: var lineEdit := focusOwner as LineEdit return lineEdit.editable if focusOwner is TextEdit: var textEdit := focusOwner as TextEdit return textEdit.editable return false func _on_interact_pressed(_data: Dictionary = {}) -> void: _try_start_private_chat() func _try_start_private_chat() -> bool: if _is_text_input_focused(): return false if not _settings_bool("allow_nearby_private", true): return false var now := Time.get_ticks_msec() if _last_private_chat_interaction_msec > 0 and now - _last_private_chat_interaction_msec < 160: return false var target := _find_private_chat_target() if target == null: return false var eventSystem := _get_event_system() if eventSystem == null: return false _last_private_chat_interaction_msec = now eventSystem.call("emit_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, { "userId": str(target.get("userId")), "username": str(target.get("username")) }) return true func _try_request_friend() -> bool: if _is_text_input_focused(): return false if not _settings_bool("allow_nearby_friend_requests", true): return false var now := Time.get_ticks_msec() if _last_friend_request_interaction_msec > 0 and now - _last_friend_request_interaction_msec < 220: return false var target := _find_private_chat_target() if target == null: return false var chat_ui := _find_chat_ui() if chat_ui == null or not chat_ui.has_method("request_friend_with_target"): return false _last_friend_request_interaction_msec = now return bool(chat_ui.call( "request_friend_with_target", str(target.get("userId")), str(target.get("username")) )) func _on_chat_login_success(_data: Dictionary) -> void: _send_world_ready() func _on_local_player_moved(data: Dictionary) -> void: var position_variant: Variant = data.get("position", null) if position_variant is Vector2: _send_position(position_variant as Vector2) func _send_world_ready() -> void: if _local_player == null: return var chatManager := _get_chat_manager() if chatManager == null or not chatManager.has_method("mark_world_ready"): return chatManager.call("mark_world_ready", _get_map_id(), _local_player.global_position) func _send_position(position: Vector2, force: bool = false) -> void: var now := Time.get_ticks_msec() if not force and now - _last_sent_at_msec < int(POSITION_SEND_INTERVAL * 1000.0): return if not force and _last_sent_position != Vector2.INF and _last_sent_position.distance_to(position) < MIN_POSITION_DELTA: return var chatManager := _get_chat_manager() if chatManager == null: return if chatManager.has_method("can_attempt_chat_connection") and not bool(chatManager.call("can_attempt_chat_connection")): return _last_sent_position = position _last_sent_at_msec = now chatManager.call("update_player_position", position.x, position.y, _get_map_id()) func _on_remote_player_joined(data: Dictionary) -> void: if not _is_event_for_current_map(data): return var user_id := str(data.get("userId", "")).strip_edges() if user_id.is_empty(): return var remote_player := _ensure_remote_player(user_id, data) var position_variant: Variant = data.get("position", null) if remote_player != null and position_variant is Vector2 and remote_player.has_method("update_position"): remote_player.call("update_position", position_variant as Vector2) func _on_remote_players_snapshot_ready(data: Dictionary) -> void: if not _is_event_for_current_map(data): return var seen_user_ids: Dictionary = {} var players_variant: Variant = data.get("players", []) if players_variant is Array: for player_variant in players_variant: if not (player_variant is Dictionary): continue var player_data: Dictionary = player_variant var user_id := str(player_data.get("userId", "")).strip_edges() if user_id.is_empty() or _is_current_user(user_id, str(player_data.get("username", ""))): continue seen_user_ids[user_id] = true var remote_player := _ensure_remote_player(user_id, player_data) var position_variant: Variant = player_data.get("position", null) if remote_player != null and position_variant is Vector2 and remote_player.has_method("update_position"): remote_player.call("update_position", position_variant as Vector2) for user_id_variant in _remote_players.keys().duplicate(): var user_id := str(user_id_variant) if seen_user_ids.has(user_id): continue var remote_player := _remote_players[user_id] as Node _remote_players.erase(user_id) if is_instance_valid(remote_player): remote_player.queue_free() for user_id_variant in _pending_remote_players.keys().duplicate(): var pendingUserId := str(user_id_variant) if not seen_user_ids.has(pendingUserId): _pending_remote_players.erase(pendingUserId) func _on_remote_player_position_updated(data: Dictionary) -> void: if not _is_event_for_current_map(data): return var user_id := str(data.get("userId", "")).strip_edges() if user_id.is_empty(): return if _is_current_user(user_id, str(data.get("username", ""))): return var position_variant: Variant = data.get("position", null) if not (position_variant is Vector2): return var remote_player := _ensure_remote_player(user_id, data) if remote_player != null and remote_player.has_method("update_position"): remote_player.call("update_position", position_variant as Vector2) func _on_remote_player_left(data: Dictionary) -> void: if not _is_event_for_current_map(data): return var user_id := str(data.get("userId", "")).strip_edges() if user_id.is_empty(): return _pending_remote_players.erase(user_id) if not _remote_players.has(user_id): return var remote_player := _remote_players[user_id] as Node _remote_players.erase(user_id) if is_instance_valid(remote_player): remote_player.queue_free() func _on_chat_message_received(data: Dictionary) -> void: if not bool(data.get("show_bubble", false)): return if not _settings_bool("show_chat_bubbles", true): return var content := str(data.get("content", "")).strip_edges() if content.is_empty(): return var target := _resolve_chat_bubble_target(data) if target == null: return _show_chat_bubble(target, content) func _ensure_remote_player(user_id: String, data: Dictionary) -> Node2D: if _is_current_user(user_id, str(data.get("username", ""))): return null var appearanceManager := get_node_or_null("/root/AppearanceManager") var skinId := str(data.get("skin_id", data.get("skinId", ""))).strip_edges() var skinAssetVariant: Variant = data.get("skin_asset", data.get("skinAsset", {})) var skinAsset: Dictionary = skinAssetVariant if skinAssetVariant is Dictionary else {} if appearanceManager != null and appearanceManager.has_method("is_remote_skin_ready"): if not bool(appearanceManager.call("is_remote_skin_ready", skinId, skinAsset)): _queue_pending_remote_player(user_id, data) if _remote_players.has(user_id): var loadingPlayer := _remote_players[user_id] as Node2D if is_instance_valid(loadingPlayer): loadingPlayer.hide() if appearanceManager.has_method("request_remote_skin"): appearanceManager.call("request_remote_skin", skinId, skinAsset) return null if _remote_players.has(user_id): var existing_player := _remote_players[user_id] as Node2D if is_instance_valid(existing_player): _apply_remote_player_metadata(existing_player, data) existing_player.show() return existing_player if _remote_players_root == null: return null var remote_player := REMOTE_PLAYER_SCENE.instantiate() as Node2D if remote_player == null: return null remote_player.name = "RemotePlayer_%s" % user_id remote_player.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST _remote_players_root.add_child(remote_player) _remote_players[user_id] = remote_player if remote_player.has_method("setup"): var setup_data := data.duplicate(true) if not setup_data.has("position"): setup_data["position"] = {"x": _local_player.global_position.x if _local_player != null else 0.0, "y": _local_player.global_position.y if _local_player != null else 0.0} remote_player.call("setup", setup_data) return remote_player func _queue_pending_remote_player(userId: String, data: Dictionary) -> void: var merged: Dictionary = (_pending_remote_players.get(userId, {}) as Dictionary).duplicate(true) for key in data.keys(): merged[key] = data[key] _pending_remote_players[userId] = merged func _on_remote_skin_ready(data: Dictionary) -> void: var skinId := str(data.get("skin_id", "")).strip_edges() if skinId.is_empty(): return for userIdVariant in _pending_remote_players.keys().duplicate(): var userId := str(userIdVariant) var pendingData: Dictionary = _pending_remote_players.get(userId, {}) var pendingSkinId := str(pendingData.get("skin_id", pendingData.get("skinId", ""))).strip_edges() if pendingSkinId != skinId: continue _pending_remote_players.erase(userId) var remotePlayer := _ensure_remote_player(userId, pendingData) var positionVariant: Variant = pendingData.get("position", null) if remotePlayer != null and positionVariant is Vector2 and remotePlayer.has_method("update_position"): remotePlayer.call("update_position", positionVariant as Vector2) func _on_remote_skin_failed(data: Dictionary) -> void: var skinId := str(data.get("skin_id", "")).strip_edges() if skinId.is_empty(): return for userIdVariant in _pending_remote_players.keys().duplicate(): var userId := str(userIdVariant) var pendingData: Dictionary = _pending_remote_players.get(userId, {}) var pendingSkinId := str(pendingData.get("skin_id", pendingData.get("skinId", ""))).strip_edges() if pendingSkinId != skinId: continue _pending_remote_players.erase(userId) var fallbackData := pendingData.duplicate(true) fallbackData["skin_id"] = "" fallbackData["skinId"] = "" fallbackData.erase("skin_asset") fallbackData.erase("skinAsset") var remotePlayer := _ensure_remote_player(userId, fallbackData) var positionVariant: Variant = pendingData.get("position", null) if remotePlayer != null and positionVariant is Vector2 and remotePlayer.has_method("update_position"): remotePlayer.call("update_position", positionVariant as Vector2) func _find_private_chat_target() -> Node2D: if _local_player == null or _remote_players.is_empty(): return null var local_position := _local_player.global_position var facing_direction := _get_local_facing_direction() var best_forward_target: Node2D = null var best_forward_distance := INF var best_any_target: Node2D = null var best_any_distance := INF for remote_player_variant in _remote_players.values(): var remote_player := remote_player_variant as Node2D if not is_instance_valid(remote_player): continue var offset := remote_player.global_position - local_position var distance := offset.length() if distance > PRIVATE_CHAT_INTERACTION_DISTANCE: continue if distance < best_any_distance: best_any_distance = distance best_any_target = remote_player if distance <= 0.01: continue var forward_dot := facing_direction.dot(offset.normalized()) if forward_dot >= PRIVATE_CHAT_FORWARD_DOT_THRESHOLD and distance < best_forward_distance: best_forward_distance = distance best_forward_target = remote_player return best_forward_target if best_forward_target != null else best_any_target func _get_local_facing_direction() -> Vector2: if _local_player != null: var last_direction := str(_local_player.get("lastDirection")) match last_direction: "up": return Vector2.UP "down": return Vector2.DOWN "left": return Vector2.LEFT "right": return Vector2.RIGHT _: pass return Vector2.DOWN func _apply_remote_player_metadata(remote_player: Node2D, data: Dictionary) -> void: var username := str(data.get("username", "")).strip_edges() if not username.is_empty(): remote_player.set("username", username) if data.has("skin_id") or data.has("skinId") or data.has("skin_asset") or data.has("skinAsset") or data.has("avatar_id") or data.has("avatarId") or data.has("cafe_companion") or data.has("cafeCompanion"): if remote_player.has_method("setup"): remote_player.call("setup", data) func _resolve_chat_bubble_target(data: Dictionary) -> Node2D: var from_user := str(data.get("from_user", data.get("username", ""))).strip_edges() var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges() var is_self := bool(data.get("is_self", false)) if is_self or (not user_id.is_empty() and _is_current_user(user_id, from_user)) or _is_current_username(from_user): return _local_player if not user_id.is_empty() and _remote_players.has(user_id): var remote_by_id := _remote_players[user_id] as Node2D if is_instance_valid(remote_by_id): return remote_by_id if from_user.is_empty(): return null for remote_player_variant in _remote_players.values(): var remote_player := remote_player_variant as Node2D if not is_instance_valid(remote_player): continue var remote_username := str(remote_player.get("username")).strip_edges() if not remote_username.is_empty() and remote_username == from_user: return remote_player return null func _show_chat_bubble(target: Node2D, text: String) -> void: var bubble := CHAT_BUBBLE_SCENE.instantiate() as Control if bubble == null: return var bubble_layer := _get_chat_bubble_layer() bubble_layer.add_child(bubble) if bubble.has_method("set_text"): bubble.call("set_text", text, target, CHAT_BUBBLE_TARGET_OFFSET) func _get_chat_bubble_layer() -> CanvasLayer: return _get_or_create_canvas_layer(CHAT_BUBBLE_LAYER_NAME, 20) func _get_or_create_canvas_layer(layerName: String, layerIndex: int) -> CanvasLayer: var root := get_tree().root var layer := root.get_node_or_null(layerName) as CanvasLayer if layer != null: return layer layer = CanvasLayer.new() layer.name = layerName layer.layer = layerIndex root.add_child(layer) return layer func _get_map_id() -> String: var configured_map_id := map_id.strip_edges() return configured_map_id if not configured_map_id.is_empty() else DEFAULT_MAP_ID func _is_event_for_current_map(data: Dictionary) -> bool: var event_map_id := str(data.get("mapId", data.get("map_id", ""))).strip_edges() return event_map_id.is_empty() or event_map_id == _get_map_id() func _is_current_user(user_id: String, username: String = "") -> bool: var authManager := get_node_or_null("/root/AuthManager") if authManager == null: return false var user: Dictionary = authManager.call("get_current_user") var current_id := str(user.get("id", user.get("userId", user.get("user_id", "")))).strip_edges() if not current_id.is_empty() and user_id == current_id: return true var current_username := str(user.get("username", "")).strip_edges() return not current_username.is_empty() and not username.strip_edges().is_empty() and current_username == username.strip_edges() func _is_current_username(username: String) -> bool: var normalized_username := username.strip_edges() if normalized_username.is_empty(): return false var authManager := get_node_or_null("/root/AuthManager") if authManager != null: var user: Dictionary = authManager.call("get_current_user") var current_username := str(user.get("username", "")).strip_edges() if not current_username.is_empty() and current_username == normalized_username: return true var chatManager := _get_chat_manager() if chatManager != null: var chat_username := str(chatManager.get("_current_username")).strip_edges() if not chat_username.is_empty() and chat_username == normalized_username: return true return false func _settings_bool(key: String, defaultValue: bool) -> bool: var settingsManager := _get_settings_manager() if settingsManager != null and settingsManager.has_method("get_bool"): return bool(settingsManager.call("get_bool", key)) return defaultValue