338 lines
8.2 KiB
GDScript
338 lines
8.2 KiB
GDScript
@tool
|
||
class_name Npc2D extends AnimatedSprite2D
|
||
|
||
signal interacted
|
||
signal talk_finished # 在 unlock player 之前发射
|
||
|
||
# 常量定义
|
||
const SPEAKING_SIGN_FADE_DURATION := 0.3
|
||
const SPEAKING_SCALE_MULTIPLIER := 1.3
|
||
|
||
enum SpeakingSignMode { HIDDEN = 0, SILENT = 1, SPEAKING = 2 }
|
||
|
||
# 导出变量
|
||
@export var snap_to_edge := true
|
||
@export var walk_to_edge_width := 25.0
|
||
@export var action_key := 4
|
||
@export var enabled := true:
|
||
set(val):
|
||
enabled = val
|
||
if is_node_ready():
|
||
_align_signs_status()
|
||
|
||
@export var sign_mark_height := 10.0:
|
||
set(val):
|
||
sign_mark_height = val
|
||
if is_node_ready() and sign_mark:
|
||
sign_mark.sign_mark_offset.y = -sign_mark_height
|
||
|
||
@export var speaking_sign_height := 60.0:
|
||
set(val):
|
||
speaking_sign_height = val
|
||
if is_node_ready() and speaking_sign:
|
||
speaking_sign.position.y = -speaking_sign_height
|
||
|
||
@export var sign_x_offset := 0.0:
|
||
set(val):
|
||
sign_x_offset = val
|
||
if is_node_ready():
|
||
_update_sign_x_positions()
|
||
|
||
@export var collision_width_and_x := Vector2(20.0, 0):
|
||
set(val):
|
||
collision_width_and_x = val
|
||
if is_node_ready():
|
||
_update_collision_shape()
|
||
|
||
# 节点引用
|
||
@onready var speaking_animation = %SpeakingAnimationPlayer
|
||
@onready var speaking_sign = %SpeakingSign2D as Node2D
|
||
@onready var sign_mark = %Sign as Sign
|
||
@onready var sign_snapper = %SignSnapper as SignSnapper
|
||
@onready var area2d = %Area2D as Area2D
|
||
|
||
# 内部变量
|
||
var ground_archive: GroundArchive
|
||
var icount: int:
|
||
set(val):
|
||
if icount == val:
|
||
return
|
||
icount = val
|
||
if ground_archive:
|
||
ground_archive.set_pair(name, "icount", val)
|
||
_align_signs_status()
|
||
|
||
var dialogue_title := ""
|
||
var dialogue_res = preload("res://asset/dialogue/npc.dialogue")
|
||
|
||
var base_scale := Vector2.ONE
|
||
var base_mod := Color.WHITE_SMOKE
|
||
var speaking_sign_tween: Tween
|
||
var speaking_sign_mode := SpeakingSignMode.HIDDEN:
|
||
set(val):
|
||
if speaking_sign_mode == val:
|
||
return
|
||
speaking_sign_mode = val
|
||
_update_speaking_sign_mode()
|
||
|
||
# 强制播放状态管理
|
||
var is_hooked := false
|
||
var hook_id := 0 # 用于追踪hook会话,避免异步问题
|
||
|
||
|
||
func _ready() -> void:
|
||
_initialize_components()
|
||
|
||
if Engine.is_editor_hint():
|
||
_setup_editor_preview()
|
||
return
|
||
|
||
_setup_game_mode()
|
||
_align_signs_status()
|
||
|
||
|
||
func _initialize_components() -> void:
|
||
# 设置标记位置
|
||
if sign_mark:
|
||
sign_mark.sign_mark_offset.y = -sign_mark_height
|
||
sign_mark.position.x = sign_x_offset
|
||
|
||
if speaking_sign:
|
||
speaking_sign.position.y = -speaking_sign_height
|
||
speaking_sign.position.x = sign_x_offset
|
||
base_scale = speaking_sign.scale
|
||
base_mod = speaking_sign.modulate
|
||
speaking_sign.modulate.a = 0.0
|
||
|
||
# 设置碰撞形状
|
||
_update_collision_shape()
|
||
|
||
# 配置 sign_snapper
|
||
if sign_snapper:
|
||
sign_snapper.action_on_arrived = action_key
|
||
sign_snapper.radius = walk_to_edge_width
|
||
sign_snapper.enabled = snap_to_edge
|
||
|
||
|
||
func _setup_editor_preview() -> void:
|
||
if speaking_sign:
|
||
speaking_sign.visible = true
|
||
speaking_sign.modulate.a = 1.0
|
||
var sprite = speaking_sign.get_node_or_null("Sprite2D")
|
||
if sprite:
|
||
sprite.position.x = -60.0
|
||
sprite.frame = 2
|
||
|
||
if sign_mark:
|
||
sign_mark.display_sign = true
|
||
|
||
|
||
func _setup_game_mode() -> void:
|
||
# 获取存档数据
|
||
ground_archive = ArchiveManager.archive.ground_archive()
|
||
icount = ground_archive.get_value(name, "icount", 0)
|
||
|
||
# 连接信号
|
||
if snap_to_edge and sign_snapper:
|
||
sign_snapper.arrived.connect(_on_interacted)
|
||
elif sign_mark:
|
||
sign_mark.interacted.connect(_on_interacted)
|
||
|
||
if sign_mark:
|
||
sign_mark.toggle_active.connect(_on_toggle_active)
|
||
|
||
visibility_changed.connect(_on_visibility_changed)
|
||
|
||
# 开始动画
|
||
if sprite_frames and animation:
|
||
play()
|
||
|
||
|
||
func _update_sign_x_positions() -> void:
|
||
if speaking_sign:
|
||
speaking_sign.position.x = sign_x_offset
|
||
if sign_mark:
|
||
sign_mark.position.x = sign_x_offset
|
||
|
||
|
||
func _update_collision_shape() -> void:
|
||
if not area2d:
|
||
return
|
||
|
||
var collision_shape = area2d.get_node_or_null("CollisionShape2D")
|
||
if collision_shape and collision_shape.shape:
|
||
collision_shape.shape.size.x = collision_width_and_x.x
|
||
area2d.position.x = collision_width_and_x.y
|
||
|
||
|
||
func _update_speaking_sign_mode() -> void:
|
||
if not speaking_sign:
|
||
return
|
||
|
||
# 清理之前的补间动画
|
||
if speaking_sign_tween and speaking_sign_tween.is_valid():
|
||
speaking_sign_tween.kill()
|
||
|
||
speaking_sign_tween = create_tween()
|
||
|
||
match speaking_sign_mode:
|
||
SpeakingSignMode.HIDDEN:
|
||
speaking_sign_tween.tween_property(
|
||
speaking_sign, "modulate:a", 0.0, SPEAKING_SIGN_FADE_DURATION
|
||
)
|
||
if speaking_animation:
|
||
speaking_animation.stop()
|
||
|
||
SpeakingSignMode.SILENT:
|
||
speaking_sign_tween.tween_property(
|
||
speaking_sign, "modulate", base_mod, SPEAKING_SIGN_FADE_DURATION
|
||
)
|
||
speaking_sign_tween.parallel().tween_property(
|
||
speaking_sign, "scale", base_scale, SPEAKING_SIGN_FADE_DURATION
|
||
)
|
||
if speaking_animation:
|
||
speaking_animation.play("speaking")
|
||
|
||
SpeakingSignMode.SPEAKING:
|
||
speaking_sign_tween.tween_property(
|
||
speaking_sign, "modulate", Color.WHITE, SPEAKING_SIGN_FADE_DURATION
|
||
)
|
||
speaking_sign_tween.parallel().tween_property(
|
||
speaking_sign,
|
||
"scale",
|
||
base_scale * SPEAKING_SCALE_MULTIPLIER,
|
||
SPEAKING_SIGN_FADE_DURATION
|
||
)
|
||
if speaking_animation:
|
||
speaking_animation.play("speaking")
|
||
|
||
|
||
func _on_visibility_changed() -> void:
|
||
_align_signs_status()
|
||
|
||
|
||
func _align_signs_status() -> void:
|
||
if not is_node_ready():
|
||
return
|
||
var is_active = enabled and is_visible_in_tree()
|
||
if sign_mark:
|
||
sign_mark.enabled = is_active
|
||
sign_mark.display_sign = icount == 0
|
||
if speaking_sign:
|
||
speaking_sign.visible = enabled and (icount > 0 or is_hooked)
|
||
|
||
|
||
func _on_toggle_active(activated: bool) -> void:
|
||
# 如果处于hook状态,不响应正常的toggle
|
||
if is_hooked:
|
||
return
|
||
if not activated:
|
||
speaking_sign_mode = SpeakingSignMode.HIDDEN
|
||
elif speaking_sign_mode == SpeakingSignMode.HIDDEN:
|
||
speaking_sign_mode = SpeakingSignMode.SILENT
|
||
|
||
|
||
func _on_interacted() -> void:
|
||
if not dialogue_title:
|
||
return
|
||
|
||
# 如果正在hook播放,先取消hook
|
||
if is_hooked:
|
||
_cancel_hook()
|
||
|
||
SceneManager.lock_player(0, action_key)
|
||
icount += 1
|
||
|
||
if ground_archive:
|
||
ground_archive.set_pair(name, "icount", icount)
|
||
|
||
DialogueManager.show_dialogue_balloon(dialogue_res, dialogue_title)
|
||
interacted.emit()
|
||
|
||
var was_out_of_range = speaking_sign_mode == SpeakingSignMode.HIDDEN
|
||
speaking_sign_mode = SpeakingSignMode.SPEAKING
|
||
|
||
await DialogueManager.dialogue_ended
|
||
|
||
speaking_sign_mode = SpeakingSignMode.HIDDEN if was_out_of_range else SpeakingSignMode.SILENT
|
||
talk_finished.emit()
|
||
SceneManager.unlock_player()
|
||
|
||
|
||
# 新增:强制播放说话动画
|
||
func hook_speaking() -> int:
|
||
# 强制显示说话气泡动画
|
||
# 返回值:hook会话ID,用于验证unhook的有效性
|
||
is_hooked = true
|
||
hook_id += 1
|
||
var current_hook_id = hook_id
|
||
|
||
# 确保speaking_sign可见
|
||
if speaking_sign and not speaking_sign.visible:
|
||
speaking_sign.visible = true
|
||
|
||
# 保存当前状态并切换到说话模式
|
||
speaking_sign_mode = SpeakingSignMode.SPEAKING
|
||
|
||
return current_hook_id
|
||
|
||
|
||
# 新增:退出强制播放
|
||
func unhook_speaking(session_id: int = -1) -> void:
|
||
# 退出强制播放模式
|
||
# 参数:
|
||
# session_id: hook会话ID,如果不匹配则忽略此次unhook
|
||
# 如果已经不在hook状态,或session_id不匹配,则忽略
|
||
if not is_hooked or (session_id != -1 and session_id != hook_id):
|
||
return
|
||
_cancel_hook()
|
||
|
||
|
||
# 新增:内部取消hook的方法
|
||
func _cancel_hook() -> void:
|
||
# 内部使用的取消hook方法
|
||
is_hooked = false
|
||
|
||
# 恢复到适当的状态
|
||
if sign_mark and sign_mark.activated:
|
||
speaking_sign_mode = SpeakingSignMode.SILENT
|
||
else:
|
||
speaking_sign_mode = SpeakingSignMode.HIDDEN
|
||
|
||
# 重新对齐显示状态
|
||
_align_signs_status()
|
||
|
||
|
||
# 新增:检查是否正在hook播放
|
||
func is_hook_speaking() -> bool:
|
||
"""返回当前是否处于强制播放状态"""
|
||
return is_hooked
|
||
|
||
|
||
func _get(property: StringName) -> Variant:
|
||
if property == "dialogue_title":
|
||
return dialogue_title
|
||
return null
|
||
|
||
|
||
func _set(property: StringName, value: Variant) -> bool:
|
||
if property == "dialogue_title":
|
||
dialogue_title = value
|
||
return true
|
||
return false
|
||
|
||
|
||
func _get_property_list() -> Array[Dictionary]:
|
||
var hint_str = ""
|
||
if Engine.is_editor_hint() and dialogue_res:
|
||
hint_str = ",".join(dialogue_res.get_ordered_titles())
|
||
|
||
return [
|
||
{
|
||
"name": "dialogue_title",
|
||
"type": TYPE_STRING,
|
||
"hint": PROPERTY_HINT_ENUM_SUGGESTION,
|
||
"hint_string": hint_str
|
||
}
|
||
]
|