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:
2026-09-08 21:37:43 +08:00
parent 175621f66c
commit fc6c3c1bd3
87 changed files with 4546 additions and 506 deletions

View File

@@ -8,27 +8,27 @@ class_name NPCController
# 主要功能:
# - 播放 NPC 待机动画
# - 响应玩家射线交互
# - 触发聊天气泡与 NPC 对话事件
# - 打开 NPC 对话框并广播 NPC 对话事件
#
# 依赖: EventSystem, EventNames, ChatBubble
# 依赖: EventSystem, EventNames, ChatUI
# 作者: Codex
# 创建时间: 2026-03-10
# ============================================================================
signal interaction_happened(text: String)
const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn")
const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer"
const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84)
const NPC_TALKED_EVENT: String = "npc_talked"
const NPC_COLLISION_LAYER: int = 2
const NPC_COLLISION_MASK: int = 1
const WORLD_SORT_Z_OFFSET: int = 2048
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
const NAMEPLATE_RENDER_SCALE: float = 0.5
const NAMEPLATE_FONT_SIZE: int = 24
const NAMEPLATE_VISUAL_HEIGHT: int = 22
const NAMEPLATE_VISUAL_MIN_WIDTH: int = 76
const NAMEPLATE_VISUAL_MAX_WIDTH: int = 118
const NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
const NAMEPLATE_FONT_SIZE: int = 16
const NAMEPLATE_VISUAL_HEIGHT: int = 15
const NAMEPLATE_VISUAL_MIN_WIDTH: int = 64
const NAMEPLATE_VISUAL_MAX_WIDTH: int = 100
const NAMEPLATE_FONT_ZH: FontFile = preload("res://assets/fonts/fusion-pixel-12px/fusion-pixel-12px-proportional-zh_hans.ttf.woff2")
const NAMEPLATE_FONT_LATIN: FontFile = preload("res://assets/fonts/fusion-pixel-12px/fusion-pixel-12px-proportional-latin.ttf.woff2")
@export var npcName: String = "NPC"
@export_multiline var dialogue: String = "欢迎来到WhaleTown我是镇长范鲸晶"
@@ -38,6 +38,7 @@ const NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var _nameplate: Label
var _nicknameFont: FontFile
func _ready() -> void:
# 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。
@@ -46,16 +47,18 @@ func _ready() -> void:
_update_nameplate()
_update_world_sort_z()
# 保持 NPC 可被玩家射线与角色碰撞识别
collision_layer = 3
collision_mask = 3
# NPC 单独占用交互层;玩家的物理掩码同时包含地图层和 NPC 层
collision_layer = NPC_COLLISION_LAYER
collision_mask = NPC_COLLISION_MASK
func _physics_process(_delta: float) -> void:
_update_world_sort_z()
# 处理玩家交互,展示气泡并向全局事件系统广播。
# 处理玩家交互,打开统一对话框并向全局事件系统广播。
func interact() -> void:
show_bubble(dialogue)
var chatUi := get_tree().root.find_child("ChatUI", true, false)
if chatUi != null and chatUi.has_method("show_npc_dialogue"):
chatUi.call("show_npc_dialogue", npcName, dialogue)
var eventSystem: Node = get_node_or_null("/root/EventSystem")
if eventSystem != null:
eventSystem.call("emit_event", NPC_TALKED_EVENT, {
@@ -65,31 +68,6 @@ func interact() -> void:
})
interaction_happened.emit(dialogue)
# 在 NPC 头顶生成一次性聊天气泡。
#
# 参数:
# text: String - 要展示的对话内容
func show_bubble(text: String) -> void:
var bubble: Control = CHAT_BUBBLE_SCENE.instantiate() as Control
if bubble == null:
return
var bubbleLayer: CanvasLayer = _get_chat_bubble_layer()
bubbleLayer.add_child(bubble)
if bubble.has_method("set_text"):
bubble.call("set_text", text, self, CHAT_BUBBLE_TARGET_OFFSET)
func _get_chat_bubble_layer() -> CanvasLayer:
var root: Window = get_tree().root
var layer: CanvasLayer = root.get_node_or_null(CHAT_BUBBLE_LAYER_NAME) as CanvasLayer
if layer != null:
return layer
layer = CanvasLayer.new()
layer.name = CHAT_BUBBLE_LAYER_NAME
layer.layer = 20
root.add_child(layer)
return layer
func _update_world_sort_z() -> void:
z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y))
@@ -123,34 +101,20 @@ func _update_nameplate() -> void:
_nameplate.clip_text = true
_nameplate.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_nameplate.mouse_filter = Control.MOUSE_FILTER_IGNORE
_nameplate.add_theme_color_override("font_color", Color(0.12, 0.20, 0.24, 1.0))
_nameplate.add_theme_color_override("font_shadow_color", Color(1.0, 1.0, 1.0, 0.85))
_nameplate.add_theme_constant_override("shadow_offset_x", 0)
_nameplate.add_theme_constant_override("shadow_offset_y", 1)
_nameplate.add_theme_color_override("font_color", Color(0.09, 0.25, 0.31, 1.0))
_nameplate.add_theme_color_override("font_outline_color", Color(1.0, 0.965, 0.88, 0.98))
_nameplate.add_theme_constant_override("outline_size", 3)
_nameplate.add_theme_font_override("font", _get_nickname_font())
_nameplate.add_theme_font_size_override("font_size", NAMEPLATE_FONT_SIZE)
_nameplate.add_theme_stylebox_override("normal", _create_nameplate_style())
_nameplate.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
func _nameplate_visual_width(displayName: String) -> int:
var estimatedWidth := displayName.length() * NAMEPLATE_VISUAL_CHAR_WIDTH + 28
return clampi(estimatedWidth, NAMEPLATE_VISUAL_MIN_WIDTH, NAMEPLATE_VISUAL_MAX_WIDTH)
var measuredWidth := _get_nickname_font().get_string_size(displayName, HORIZONTAL_ALIGNMENT_LEFT, -1, NAMEPLATE_FONT_SIZE).x
var visualWidth := ceili(measuredWidth * NAMEPLATE_RENDER_SCALE + 20.0)
return clampi(visualWidth, NAMEPLATE_VISUAL_MIN_WIDTH, NAMEPLATE_VISUAL_MAX_WIDTH)
func _create_nameplate_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(1.0, 0.988, 0.955, 0.96)
style.border_color = Color(0.18, 0.34, 0.38, 0.92)
style.border_width_left = 2
style.border_width_top = 2
style.border_width_right = 2
style.border_width_bottom = 2
style.corner_radius_top_left = 12
style.corner_radius_top_right = 12
style.corner_radius_bottom_left = 12
style.corner_radius_bottom_right = 12
style.content_margin_left = 12
style.content_margin_top = 4
style.content_margin_right = 12
style.content_margin_bottom = 4
style.shadow_color = Color(0.05, 0.08, 0.09, 0.18)
style.shadow_size = 4
style.shadow_offset = Vector2(0, 2)
return style
func _get_nickname_font() -> FontFile:
if _nicknameFont == null:
_nicknameFont = NAMEPLATE_FONT_ZH.duplicate() as FontFile
_nicknameFont.fallbacks = [NAMEPLATE_FONT_LATIN]
return _nicknameFont