2025-01-10 09:53:12 +00:00
|
|
|
|
@tool
|
2025-05-14 20:43:55 +00:00
|
|
|
|
class_name Npc2D extends AnimatedSprite2D
|
|
|
|
|
|
|
|
|
|
signal interacted
|
2025-07-16 07:23:00 +00:00
|
|
|
|
signal talk_finished # 在 unlock player 之前发射
|
2024-12-26 13:58:37 +00:00
|
|
|
|
|
2025-07-16 07:23:00 +00:00
|
|
|
|
# 常量定义
|
|
|
|
|
const SPEAKING_SIGN_FADE_DURATION := 0.3
|
|
|
|
|
const SPEAKING_SCALE_MULTIPLIER := 1.3
|
|
|
|
|
|
|
|
|
|
enum SpeakingSignMode { HIDDEN = 0, SILENT = 1, SPEAKING = 2 }
|
|
|
|
|
|
|
|
|
|
# 导出变量
|
2025-06-26 09:38:51 +00:00
|
|
|
|
@export var snap_to_edge := true
|
2025-06-25 10:50:02 +00:00
|
|
|
|
@export var walk_to_edge_width := 25.0
|
|
|
|
|
@export var action_key := 4
|
2025-06-13 08:03:19 +00:00
|
|
|
|
@export var enabled := true:
|
|
|
|
|
set(val):
|
|
|
|
|
enabled = val
|
|
|
|
|
if is_node_ready():
|
2025-06-29 14:57:02 +00:00
|
|
|
|
_align_signs_status()
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
2025-06-29 14:57:02 +00:00
|
|
|
|
@export var sign_mark_height := 10.0:
|
2025-02-12 07:03:41 +00:00
|
|
|
|
set(val):
|
2025-06-29 14:57:02 +00:00
|
|
|
|
sign_mark_height = val
|
2025-07-16 07:23:00 +00:00
|
|
|
|
if is_node_ready() and sign_mark:
|
2025-06-29 14:57:02 +00:00
|
|
|
|
sign_mark.sign_mark_offset.y = -sign_mark_height
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
2025-06-29 14:57:02 +00:00
|
|
|
|
@export var speaking_sign_height := 60.0:
|
|
|
|
|
set(val):
|
|
|
|
|
speaking_sign_height = val
|
2025-07-16 07:23:00 +00:00
|
|
|
|
if is_node_ready() and speaking_sign:
|
2025-06-29 14:57:02 +00:00
|
|
|
|
speaking_sign.position.y = -speaking_sign_height
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
2025-06-29 14:57:02 +00:00
|
|
|
|
@export var sign_x_offset := 0.0:
|
|
|
|
|
set(val):
|
|
|
|
|
sign_x_offset = val
|
|
|
|
|
if is_node_ready():
|
2025-07-16 07:23:00 +00:00
|
|
|
|
_update_sign_x_positions()
|
|
|
|
|
|
2025-06-25 10:50:02 +00:00
|
|
|
|
@export var collision_width_and_x := Vector2(20.0, 0):
|
|
|
|
|
set(val):
|
|
|
|
|
collision_width_and_x = val
|
|
|
|
|
if is_node_ready():
|
2025-07-16 07:23:00 +00:00
|
|
|
|
_update_collision_shape()
|
|
|
|
|
|
|
|
|
|
# 节点引用
|
2024-12-27 07:56:45 +00:00
|
|
|
|
@onready var speaking_animation = %SpeakingAnimationPlayer
|
|
|
|
|
@onready var speaking_sign = %SpeakingSign2D as Node2D
|
2025-01-08 00:51:09 +00:00
|
|
|
|
@onready var sign_mark = %Sign as Sign
|
2025-06-25 10:50:02 +00:00
|
|
|
|
@onready var sign_snapper = %SignSnapper as SignSnapper
|
2024-12-26 13:58:37 +00:00
|
|
|
|
@onready var area2d = %Area2D as Area2D
|
|
|
|
|
|
2025-07-16 07:23:00 +00:00
|
|
|
|
# 内部变量
|
2025-06-29 14:57:02 +00:00
|
|
|
|
var ground_archive: GroundArchive
|
|
|
|
|
var icount: int:
|
|
|
|
|
set(val):
|
2025-07-16 07:23:00 +00:00
|
|
|
|
if icount == val:
|
|
|
|
|
return
|
2025-06-29 14:57:02 +00:00
|
|
|
|
icount = val
|
2025-07-16 07:23:00 +00:00
|
|
|
|
if ground_archive:
|
|
|
|
|
ground_archive.set_pair(name, "icount", val)
|
2025-06-29 14:57:02 +00:00
|
|
|
|
_align_signs_status()
|
|
|
|
|
|
2025-01-10 09:53:12 +00:00
|
|
|
|
var dialogue_title := ""
|
|
|
|
|
var dialogue_res = preload("res://asset/dialogue/npc.dialogue")
|
|
|
|
|
|
2025-06-13 08:03:19 +00:00
|
|
|
|
var base_scale := Vector2.ONE
|
|
|
|
|
var base_mod := Color.WHITE_SMOKE
|
2025-06-30 10:33:40 +00:00
|
|
|
|
var speaking_sign_tween: Tween
|
2025-07-16 07:23:00 +00:00
|
|
|
|
var speaking_sign_mode := SpeakingSignMode.HIDDEN:
|
2025-06-30 10:33:40 +00:00
|
|
|
|
set(val):
|
2025-07-16 07:23:00 +00:00
|
|
|
|
if speaking_sign_mode == val:
|
|
|
|
|
return
|
|
|
|
|
speaking_sign_mode = val
|
|
|
|
|
_update_speaking_sign_mode()
|
|
|
|
|
|
|
|
|
|
# 强制播放状态管理
|
|
|
|
|
var is_hooked := false
|
|
|
|
|
var hook_id := 0 # 用于追踪hook会话,避免异步问题
|
2025-06-30 10:33:40 +00:00
|
|
|
|
|
2025-06-25 10:50:02 +00:00
|
|
|
|
|
2024-12-26 13:58:37 +00:00
|
|
|
|
func _ready() -> void:
|
2025-07-16 07:23:00 +00:00
|
|
|
|
_initialize_components()
|
|
|
|
|
|
2025-01-21 10:52:36 +00:00
|
|
|
|
if Engine.is_editor_hint():
|
2025-07-16 07:23:00 +00:00
|
|
|
|
_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:
|
2025-02-12 07:03:41 +00:00
|
|
|
|
speaking_sign.visible = true
|
2025-06-30 10:33:40 +00:00
|
|
|
|
speaking_sign.modulate.a = 1.0
|
2025-07-16 07:23:00 +00:00
|
|
|
|
var sprite = speaking_sign.get_node_or_null("Sprite2D")
|
|
|
|
|
if sprite:
|
|
|
|
|
sprite.position.x = -60.0
|
|
|
|
|
sprite.frame = 2
|
|
|
|
|
|
|
|
|
|
if sign_mark:
|
2025-06-29 14:57:02 +00:00
|
|
|
|
sign_mark.display_sign = true
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _setup_game_mode() -> void:
|
|
|
|
|
# 获取存档数据
|
2025-06-29 14:57:02 +00:00
|
|
|
|
ground_archive = ArchiveManager.archive.ground_archive()
|
|
|
|
|
icount = ground_archive.get_value(name, "icount", 0)
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
|
|
|
|
# 连接信号
|
|
|
|
|
if snap_to_edge and sign_snapper:
|
2025-06-25 10:50:02 +00:00
|
|
|
|
sign_snapper.arrived.connect(_on_interacted)
|
2025-07-16 07:23:00 +00:00
|
|
|
|
elif sign_mark:
|
2025-06-25 10:50:02 +00:00
|
|
|
|
sign_mark.interacted.connect(_on_interacted)
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
|
|
|
|
if sign_mark:
|
|
|
|
|
sign_mark.toggle_active.connect(_on_toggle_active)
|
|
|
|
|
|
2025-06-06 16:19:37 +00:00
|
|
|
|
visibility_changed.connect(_on_visibility_changed)
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
|
|
|
|
# 开始动画
|
2025-06-13 08:03:19 +00:00
|
|
|
|
if sprite_frames and animation:
|
|
|
|
|
play()
|
2025-06-06 16:19:37 +00:00
|
|
|
|
|
2025-06-14 08:46:32 +00:00
|
|
|
|
|
2025-07-16 07:23:00 +00:00
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2025-06-06 16:19:37 +00:00
|
|
|
|
func _on_visibility_changed() -> void:
|
2025-06-29 14:57:02 +00:00
|
|
|
|
_align_signs_status()
|
2025-06-13 08:03:19 +00:00
|
|
|
|
|
2025-06-14 08:46:32 +00:00
|
|
|
|
|
2025-07-16 07:23:00 +00:00
|
|
|
|
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)
|
2025-06-14 08:46:32 +00:00
|
|
|
|
|
2025-02-12 07:03:41 +00:00
|
|
|
|
|
|
|
|
|
func _on_toggle_active(activated: bool) -> void:
|
2025-07-16 07:23:00 +00:00
|
|
|
|
# 如果处于hook状态,不响应正常的toggle
|
|
|
|
|
if is_hooked:
|
|
|
|
|
return
|
2025-07-11 14:54:26 +00:00
|
|
|
|
if not activated:
|
2025-07-16 07:23:00 +00:00
|
|
|
|
speaking_sign_mode = SpeakingSignMode.HIDDEN
|
|
|
|
|
elif speaking_sign_mode == SpeakingSignMode.HIDDEN:
|
|
|
|
|
speaking_sign_mode = SpeakingSignMode.SILENT
|
2024-12-27 07:56:45 +00:00
|
|
|
|
|
2025-06-25 10:50:02 +00:00
|
|
|
|
|
2024-12-26 13:58:37 +00:00
|
|
|
|
func _on_interacted() -> void:
|
2025-07-16 07:23:00 +00:00
|
|
|
|
if not dialogue_title:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 如果正在hook播放,先取消hook
|
|
|
|
|
if is_hooked:
|
|
|
|
|
_cancel_hook()
|
|
|
|
|
|
|
|
|
|
SceneManager.lock_player(0, action_key)
|
|
|
|
|
icount += 1
|
|
|
|
|
|
|
|
|
|
if ground_archive:
|
2025-06-29 14:57:02 +00:00
|
|
|
|
ground_archive.set_pair(name, "icount", icount)
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
|
|
|
|
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
|
2025-01-10 09:53:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]:
|
2025-01-23 02:01:53 +00:00
|
|
|
|
var hint_str = ""
|
2025-07-16 07:23:00 +00:00
|
|
|
|
if Engine.is_editor_hint() and dialogue_res:
|
2025-01-23 02:01:53 +00:00
|
|
|
|
hint_str = ",".join(dialogue_res.get_ordered_titles())
|
2025-07-16 07:23:00 +00:00
|
|
|
|
|
2025-01-10 09:53:12 +00:00
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
"name": "dialogue_title",
|
|
|
|
|
"type": TYPE_STRING,
|
|
|
|
|
"hint": PROPERTY_HINT_ENUM_SUGGESTION,
|
2025-01-23 02:01:53 +00:00
|
|
|
|
"hint_string": hint_str
|
2025-01-10 09:53:12 +00:00
|
|
|
|
}
|
|
|
|
|
]
|