forked from xiangwang25/whale-town-front-v2
- 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
121 lines
4.8 KiB
GDScript
121 lines
4.8 KiB
GDScript
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
|