Files
whale-town-front-v2/scenes/characters/NPCController.gd
xiangwang fc6c3c1bd3 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
2026-09-08 21:37:43 +08:00

121 lines
4.8 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 CharacterBody2D
class_name NPCController
# ============================================================================
# 文件名: NPCController.gd
# 作用: 通用 NPC 控制器,负责角色待机表现与交互对话
#
# 主要功能:
# - 播放 NPC 待机动画
# - 响应玩家射线交互
# - 打开 NPC 对话框并广播 NPC 对话事件
#
# 依赖: EventSystem, EventNames, ChatUI
# 作者: Codex
# 创建时间: 2026-03-10
# ============================================================================
signal interaction_happened(text: String)
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 = 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我是镇长范鲸晶"
@export var showNameplate: bool = false
@export var nameplateOffsetY: float = -112.0
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var _nameplate: Label
var _nicknameFont: FontFile
func _ready() -> void:
# 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。
if animation_player.has_animation("idle"):
animation_player.play("idle")
_update_nameplate()
_update_world_sort_z()
# 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:
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, {
"npc": self,
"npc_name": npcName,
"dialogue": dialogue
})
interaction_happened.emit(dialogue)
func _update_world_sort_z() -> void:
z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y))
func _update_nameplate() -> void:
if not showNameplate:
if is_instance_valid(_nameplate):
_nameplate.visible = false
return
if not is_instance_valid(_nameplate):
_nameplate = Label.new()
_nameplate.name = "Nameplate"
add_child(_nameplate)
var displayName := npcName.strip_edges()
if displayName.is_empty():
displayName = "NPC"
var visualWidth := _nameplate_visual_width(displayName)
var renderWidth := ceili(float(visualWidth) / NAMEPLATE_RENDER_SCALE)
var renderHeight := ceili(float(NAMEPLATE_VISUAL_HEIGHT) / NAMEPLATE_RENDER_SCALE)
_nameplate.theme = WORLD_TEXT_THEME
_nameplate.text = displayName
_nameplate.visible = true
_nameplate.z_index = 30
_nameplate.scale = Vector2.ONE * NAMEPLATE_RENDER_SCALE
_nameplate.position = Vector2(float(visualWidth) * -0.5, nameplateOffsetY)
_nameplate.custom_minimum_size = Vector2(renderWidth, renderHeight)
_nameplate.size = _nameplate.custom_minimum_size
_nameplate.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_nameplate.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_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.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", StyleBoxEmpty.new())
func _nameplate_visual_width(displayName: String) -> int:
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 _get_nickname_font() -> FontFile:
if _nicknameFont == null:
_nicknameFont = NAMEPLATE_FONT_ZH.duplicate() as FontFile
_nicknameFont.fallbacks = [NAMEPLATE_FONT_LATIN]
return _nicknameFont