Files
whale-town-front-v2/scenes/Maps/MapMultiplayerController.gd

540 lines
21 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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