feat: expand multiplayer, chat, and release support
- add network NPC synchronization and dialogue interactions - add world bulletin publishing and display - improve authentication, session refresh, and appearance sync - synchronize player direction and movement animations - improve input focus and progressive Web loading - add macOS/Windows builds and deployment configuration - include required fonts, shaders, and runtime assets
This commit is contained in:
@@ -7,17 +7,35 @@ class_name RemotePlayer
|
||||
|
||||
var userId: String = ""
|
||||
var username: String = ""
|
||||
var skinId: String = ""
|
||||
const DEFAULT_REMOTE_SKIN_ID: String = "classic_whale"
|
||||
|
||||
var skinId: String = DEFAULT_REMOTE_SKIN_ID
|
||||
var skinAsset: Dictionary = {}
|
||||
var avatarId: String = ""
|
||||
var targetPosition: Vector2 = Vector2.ZERO
|
||||
var cafeCompanionData: Dictionary = {}
|
||||
var movementState: String = "idle"
|
||||
var lastSequence: int = -1
|
||||
var _hasSyncedDirection: bool = false
|
||||
var _nicknameFont: FontFile
|
||||
var _cachedNameLabelText: String = ""
|
||||
var _cachedNameLabelPersona: String = ""
|
||||
var _cachedNameLabelDirection: String = ""
|
||||
var _cachedNameLabelVisible: bool = false
|
||||
|
||||
# 内部状态
|
||||
var lastDirection: String = "down"
|
||||
const WORLD_SORT_Z_OFFSET = 2048
|
||||
const WALK_ANIMATION_LENGTH = 0.8
|
||||
const NAME_LABEL_OFFSET: Vector2 = Vector2(-70, -112)
|
||||
const NAME_LABEL_RENDER_SCALE: float = 0.5
|
||||
const NAME_LABEL_FONT_SIZE: int = 16
|
||||
const NAME_LABEL_VISUAL_HEIGHT: int = 15
|
||||
const NAME_LABEL_VISUAL_MIN_WIDTH: int = 64
|
||||
const NAME_LABEL_VISUAL_MAX_WIDTH: int = 100
|
||||
const NAME_LABEL_VISUAL_CHAR_WIDTH: int = 10
|
||||
const NAME_LABEL_OFFSET_Y: float = -72.0
|
||||
const NAME_LABEL_SIDE_OFFSET_Y: float = -84.0
|
||||
const NAME_LABEL_BACK_OFFSET_Y: float = -78.0
|
||||
const CAFE_NAME_LABEL_RENDER_SCALE: float = 0.5
|
||||
const CAFE_NAME_LABEL_FONT_SIZE: int = 24
|
||||
const CAFE_NAME_LABEL_VISUAL_HEIGHT: int = 22
|
||||
@@ -26,6 +44,8 @@ const CAFE_NAME_LABEL_VISUAL_MAX_WIDTH: int = 118
|
||||
const CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH: int = 12
|
||||
const CAFE_NAME_LABEL_OFFSET_Y: float = -96.0
|
||||
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
||||
const NAME_LABEL_FONT_ZH: FontFile = preload("res://assets/fonts/fusion-pixel-12px/fusion-pixel-12px-proportional-zh_hans.ttf.woff2")
|
||||
const NAME_LABEL_FONT_LATIN: FontFile = preload("res://assets/fonts/fusion-pixel-12px/fusion-pixel-12px-proportional-latin.ttf.woff2")
|
||||
const DIRECTION_ROWS: Dictionary = {
|
||||
"down": 0,
|
||||
"up": 1,
|
||||
@@ -73,37 +93,15 @@ func _process(delta: float) -> void:
|
||||
global_position = newPos
|
||||
_update_world_sort_z()
|
||||
else:
|
||||
# 距离很近时直接吸附并播放待机动画
|
||||
# 距离很近时吸附;动画状态由发送端的 idle/walk 决定。
|
||||
global_position = targetPosition
|
||||
_update_world_sort_z()
|
||||
_play_idle_animation()
|
||||
_play_current_animation()
|
||||
|
||||
# 统一初始化方法
|
||||
# data: 包含 camelCase 字段的字典 (userId, username, position 等)
|
||||
func setup(data: Dictionary) -> void:
|
||||
if data.has("userId"):
|
||||
userId = data.userId
|
||||
if data.has("username"):
|
||||
username = str(data.username)
|
||||
if data.has("skin_id"):
|
||||
skinId = str(data.get("skin_id", ""))
|
||||
elif data.has("skinId"):
|
||||
skinId = str(data.get("skinId", ""))
|
||||
if data.has("skin_asset"):
|
||||
var skinAssetData: Variant = data.get("skin_asset", {})
|
||||
skinAsset = skinAssetData if skinAssetData is Dictionary else {}
|
||||
elif data.has("skinAsset"):
|
||||
var skinAssetPayload: Variant = data.get("skinAsset", {})
|
||||
skinAsset = skinAssetPayload if skinAssetPayload is Dictionary else {}
|
||||
if data.has("avatar_id"):
|
||||
avatarId = str(data.get("avatar_id", ""))
|
||||
elif data.has("avatarId"):
|
||||
avatarId = str(data.get("avatarId", ""))
|
||||
if data.has("cafe_companion") or data.has("cafeCompanion"):
|
||||
cafeCompanionData = _normalize_cafe_companion_data(data.get("cafe_companion", data.get("cafeCompanion", null)))
|
||||
_apply_appearance()
|
||||
_configure_cafe_companion_target()
|
||||
_update_name_label()
|
||||
update_metadata(data)
|
||||
|
||||
if data.has("position"):
|
||||
var positionData: Variant = data.position
|
||||
@@ -111,15 +109,73 @@ func setup(data: Dictionary) -> void:
|
||||
global_position = positionData
|
||||
targetPosition = positionData
|
||||
_update_world_sort_z()
|
||||
elif positionData.has("x") and positionData.has("y"):
|
||||
elif positionData is Dictionary and positionData.has("x") and positionData.has("y"):
|
||||
var newPos := Vector2(positionData.x, positionData.y)
|
||||
global_position = newPos
|
||||
targetPosition = newPos
|
||||
_update_world_sort_z()
|
||||
_apply_movement_state(data)
|
||||
|
||||
func update_metadata(data: Dictionary) -> void:
|
||||
var appearanceChanged := false
|
||||
var companionChanged := false
|
||||
if data.has("userId"):
|
||||
userId = data.userId
|
||||
if data.has("username"):
|
||||
username = str(data.username)
|
||||
if data.has("skin_id"):
|
||||
var nextSkinId := _normalize_remote_skin_id(str(data.get("skin_id", "")))
|
||||
appearanceChanged = appearanceChanged or nextSkinId != skinId
|
||||
skinId = nextSkinId
|
||||
elif data.has("skinId"):
|
||||
var nextSkinId := _normalize_remote_skin_id(str(data.get("skinId", "")))
|
||||
appearanceChanged = appearanceChanged or nextSkinId != skinId
|
||||
skinId = nextSkinId
|
||||
if data.has("skin_asset"):
|
||||
var skinAssetData: Variant = data.get("skin_asset", {})
|
||||
var nextSkinAsset: Dictionary = skinAssetData if skinAssetData is Dictionary else {}
|
||||
appearanceChanged = appearanceChanged or nextSkinAsset != skinAsset
|
||||
skinAsset = nextSkinAsset
|
||||
elif data.has("skinAsset"):
|
||||
var skinAssetPayload: Variant = data.get("skinAsset", {})
|
||||
var nextSkinAsset: Dictionary = skinAssetPayload if skinAssetPayload is Dictionary else {}
|
||||
appearanceChanged = appearanceChanged or nextSkinAsset != skinAsset
|
||||
skinAsset = nextSkinAsset
|
||||
if data.has("avatar_id"):
|
||||
avatarId = str(data.get("avatar_id", ""))
|
||||
elif data.has("avatarId"):
|
||||
avatarId = str(data.get("avatarId", ""))
|
||||
if data.has("cafe_companion") or data.has("cafeCompanion"):
|
||||
var nextCompanionData := _normalize_cafe_companion_data(data.get("cafe_companion", data.get("cafeCompanion", null)))
|
||||
companionChanged = nextCompanionData != cafeCompanionData
|
||||
cafeCompanionData = nextCompanionData
|
||||
if appearanceChanged:
|
||||
_apply_appearance()
|
||||
if companionChanged:
|
||||
_configure_cafe_companion_target()
|
||||
_update_name_label()
|
||||
|
||||
func _normalize_remote_skin_id(value: String) -> String:
|
||||
var normalized := value.strip_edges()
|
||||
if normalized.is_empty() or normalized == "pending_initial_skin":
|
||||
return DEFAULT_REMOTE_SKIN_ID
|
||||
return normalized
|
||||
|
||||
# 更新目标位置
|
||||
func update_position(newPos: Vector2) -> void:
|
||||
func update_position(newPos: Vector2, direction: String = "", nextMovementState: String = "walk", sequence: int = -1) -> void:
|
||||
if sequence >= 0 and lastSequence >= 0 and sequence <= lastSequence:
|
||||
return
|
||||
if sequence >= 0:
|
||||
lastSequence = sequence
|
||||
var normalizedDirection := _normalize_direction(direction)
|
||||
if not normalizedDirection.is_empty():
|
||||
lastDirection = normalizedDirection
|
||||
_hasSyncedDirection = true
|
||||
_update_name_label()
|
||||
movementState = "walk" if nextMovementState.strip_edges().to_lower() == "walk" else "idle"
|
||||
targetPosition = newPos
|
||||
if global_position.distance_to(targetPosition) <= 1.0:
|
||||
_play_current_animation()
|
||||
|
||||
func _update_world_sort_z() -> void:
|
||||
z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y))
|
||||
@@ -128,20 +184,43 @@ func _update_animation(moveVec: Vector2) -> void:
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
# 确定主方向
|
||||
if abs(moveVec.x) > abs(moveVec.y):
|
||||
if moveVec.x > 0:
|
||||
lastDirection = "right"
|
||||
# 新协议使用发送端方向;兼容旧位置包时再从位移向量推断。
|
||||
if not _hasSyncedDirection:
|
||||
if abs(moveVec.x) > abs(moveVec.y):
|
||||
if moveVec.x > 0:
|
||||
lastDirection = "right"
|
||||
else:
|
||||
lastDirection = "left"
|
||||
else:
|
||||
lastDirection = "left"
|
||||
else:
|
||||
if moveVec.y > 0:
|
||||
lastDirection = "down"
|
||||
else:
|
||||
lastDirection = "up"
|
||||
if moveVec.y > 0:
|
||||
lastDirection = "down"
|
||||
else:
|
||||
lastDirection = "up"
|
||||
|
||||
animation_player.play("walk_" + lastDirection)
|
||||
|
||||
func _apply_movement_state(data: Dictionary) -> void:
|
||||
var normalizedDirection := _normalize_direction(str(data.get("direction", "")))
|
||||
if not normalizedDirection.is_empty():
|
||||
lastDirection = normalizedDirection
|
||||
_hasSyncedDirection = true
|
||||
_update_name_label()
|
||||
movementState = "walk" if str(data.get("movement_state", data.get("movementState", "idle"))).strip_edges().to_lower() == "walk" else "idle"
|
||||
lastSequence = int(data.get("sequence", lastSequence))
|
||||
_play_current_animation()
|
||||
|
||||
func _normalize_direction(value: String) -> String:
|
||||
var normalized := value.strip_edges().to_lower()
|
||||
return normalized if normalized in ["down", "up", "right", "left"] else ""
|
||||
|
||||
func _play_current_animation() -> void:
|
||||
if animation_player == null:
|
||||
return
|
||||
if movementState == "walk":
|
||||
animation_player.play("walk_" + lastDirection)
|
||||
else:
|
||||
_play_idle_animation()
|
||||
|
||||
func _play_idle_animation() -> void:
|
||||
if animation_player:
|
||||
animation_player.play("idle_" + lastDirection)
|
||||
@@ -206,25 +285,26 @@ func _create_name_label() -> void:
|
||||
return
|
||||
_nameLabel = Label.new()
|
||||
_nameLabel.name = "NameLabel"
|
||||
_nameLabel.position = NAME_LABEL_OFFSET
|
||||
_nameLabel.custom_minimum_size = Vector2(140, 28)
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424))
|
||||
_nameLabel.add_theme_font_size_override("font_size", 14)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_name_label_style())
|
||||
add_child(_nameLabel)
|
||||
|
||||
func _update_name_label() -> void:
|
||||
if not is_instance_valid(_nameLabel):
|
||||
return
|
||||
var personaName := str(cafeCompanionData.get("persona_name", "")).strip_edges()
|
||||
var displayName := personaName if not personaName.is_empty() else (username if not username.strip_edges().is_empty() else "玩家")
|
||||
var showName := true if not personaName.is_empty() else (_is_guest_mode() or _settings_bool("show_name_always", false))
|
||||
if displayName == _cachedNameLabelText and personaName == _cachedNameLabelPersona and lastDirection == _cachedNameLabelDirection and showName == _cachedNameLabelVisible:
|
||||
return
|
||||
if not personaName.is_empty():
|
||||
_configure_cafe_name_label(personaName)
|
||||
_nameLabel.visible = true
|
||||
return
|
||||
_configure_default_name_label()
|
||||
_nameLabel.text = username if not username.strip_edges().is_empty() else "玩家"
|
||||
_nameLabel.visible = _settings_bool("show_name_always", false)
|
||||
else:
|
||||
_configure_default_name_label()
|
||||
_nameLabel.text = displayName
|
||||
_nameLabel.visible = showName
|
||||
_cachedNameLabelText = displayName
|
||||
_cachedNameLabelPersona = personaName
|
||||
_cachedNameLabelDirection = lastDirection
|
||||
_cachedNameLabelVisible = showName
|
||||
|
||||
func _configure_cafe_name_label(personaName: String) -> void:
|
||||
var displayName := personaName.strip_edges()
|
||||
@@ -246,30 +326,61 @@ func _configure_cafe_name_label(personaName: String) -> void:
|
||||
_nameLabel.clip_text = true
|
||||
_nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.12, 0.20, 0.24, 1.0))
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.09, 0.25, 0.31, 1.0))
|
||||
_nameLabel.add_theme_color_override("font_shadow_color", Color(1.0, 1.0, 1.0, 0.85))
|
||||
_nameLabel.add_theme_constant_override("shadow_offset_x", 0)
|
||||
_nameLabel.add_theme_constant_override("shadow_offset_y", 1)
|
||||
_nameLabel.remove_theme_color_override("font_outline_color")
|
||||
_nameLabel.remove_theme_constant_override("outline_size")
|
||||
_nameLabel.add_theme_font_size_override("font_size", CAFE_NAME_LABEL_FONT_SIZE)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_cafe_name_label_style())
|
||||
|
||||
func _configure_default_name_label() -> void:
|
||||
_nameLabel.theme = null
|
||||
_nameLabel.position = NAME_LABEL_OFFSET
|
||||
_nameLabel.scale = Vector2.ONE
|
||||
_nameLabel.custom_minimum_size = Vector2(140, 28)
|
||||
var displayName := username if not username.strip_edges().is_empty() else "玩家"
|
||||
var visualWidth := _name_label_visual_width(displayName)
|
||||
var renderWidth := ceili(float(visualWidth) / NAME_LABEL_RENDER_SCALE)
|
||||
var renderHeight := ceili(float(NAME_LABEL_VISUAL_HEIGHT) / NAME_LABEL_RENDER_SCALE)
|
||||
|
||||
_nameLabel.theme = WORLD_TEXT_THEME
|
||||
_nameLabel.z_index = 30
|
||||
_nameLabel.scale = Vector2.ONE * NAME_LABEL_RENDER_SCALE
|
||||
_nameLabel.position = Vector2(float(visualWidth) * -0.5, _name_label_offset_y())
|
||||
_nameLabel.custom_minimum_size = Vector2(renderWidth, renderHeight)
|
||||
_nameLabel.size = _nameLabel.custom_minimum_size
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
_nameLabel.clip_text = false
|
||||
_nameLabel.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
|
||||
_nameLabel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424))
|
||||
_nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_nameLabel.clip_text = true
|
||||
_nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.09, 0.25, 0.31, 1.0))
|
||||
_nameLabel.remove_theme_color_override("font_shadow_color")
|
||||
_nameLabel.remove_theme_constant_override("shadow_offset_x")
|
||||
_nameLabel.remove_theme_constant_override("shadow_offset_y")
|
||||
_nameLabel.add_theme_font_size_override("font_size", 14)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_name_label_style())
|
||||
_nameLabel.add_theme_color_override("font_outline_color", Color(1.0, 0.965, 0.88, 0.98))
|
||||
_nameLabel.add_theme_constant_override("outline_size", 3)
|
||||
_nameLabel.add_theme_font_override("font", _get_nickname_font())
|
||||
_nameLabel.add_theme_font_size_override("font_size", NAME_LABEL_FONT_SIZE)
|
||||
_nameLabel.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
|
||||
|
||||
func _name_label_visual_width(displayName: String) -> int:
|
||||
var measuredWidth := _get_nickname_font().get_string_size(displayName, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_LABEL_FONT_SIZE).x
|
||||
var visualWidth := ceili(measuredWidth * NAME_LABEL_RENDER_SCALE + 20.0)
|
||||
return clampi(visualWidth, NAME_LABEL_VISUAL_MIN_WIDTH, NAME_LABEL_VISUAL_MAX_WIDTH)
|
||||
|
||||
func _get_nickname_font() -> FontFile:
|
||||
if _nicknameFont == null:
|
||||
_nicknameFont = NAME_LABEL_FONT_ZH.duplicate() as FontFile
|
||||
_nicknameFont.fallbacks = [NAME_LABEL_FONT_LATIN]
|
||||
return _nicknameFont
|
||||
|
||||
func _name_label_offset_y() -> float:
|
||||
match lastDirection:
|
||||
"left", "right":
|
||||
return NAME_LABEL_SIDE_OFFSET_Y
|
||||
"up":
|
||||
return NAME_LABEL_BACK_OFFSET_Y
|
||||
_:
|
||||
return NAME_LABEL_OFFSET_Y
|
||||
|
||||
func _cafe_name_label_visual_width(displayName: String) -> int:
|
||||
var estimatedWidth := displayName.length() * CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH + 28
|
||||
@@ -340,18 +451,9 @@ func _settings_bool(key: String, defaultValue: bool) -> bool:
|
||||
return bool(settingsManager.call("get_bool", key))
|
||||
return defaultValue
|
||||
|
||||
func _create_name_label_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 1.0, 1.0, 0.74)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
style.content_margin_left = 8
|
||||
style.content_margin_right = 8
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
return style
|
||||
func _is_guest_mode() -> bool:
|
||||
var chatManager := get_node_or_null("/root/ChatManager")
|
||||
return chatManager != null and chatManager.has_method("is_guest_mode") and bool(chatManager.call("is_guest_mode"))
|
||||
|
||||
func _create_cafe_name_label_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
|
||||
Reference in New Issue
Block a user