xiandie/scene/entity/npc.gd

373 lines
9.4 KiB
GDScript3
Raw Normal View History

2025-01-10 09:53:12 +00:00
@tool
2025-05-14 20:43:55 +00:00
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()
2025-07-17 08:43:30 +00:00
# DialogueLine 播放 hook_character_name 的对话时,显示 speaking 标志
# 为空时不 hook
@export var hook_character_name := ""
# 节点引用
@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
@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()
2025-01-10 09:53:12 +00:00
var dialogue_title := ""
var dialogue_res = preload("res://asset/dialogue/npc.dialogue")
var base_scale := Vector2.ONE
var base_mod := Color.WHITE_SMOKE
2025-06-30 10:33:40 +00:00
var speaking_sign_tween: Tween
var speaking_sign_mode := SpeakingSignMode.HIDDEN:
2025-06-30 10:33:40 +00:00
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会话避免异步问题
2025-06-30 10:33:40 +00:00
func _ready() -> void:
_initialize_components()
2025-01-21 10:52:36 +00:00
if Engine.is_editor_hint():
_setup_editor_preview()
return
_setup_game_mode()
_align_signs_status()
2025-07-17 08:43:30 +00:00
DialogueManager.dialogue_ended.connect(_on_dialogue_ended)
DialogueManager.got_dialogue.connect(_on_got_dialogue)
var _last_line_session := -1
func _on_got_dialogue(line: DialogueLine) -> void:
if not hook_character_name:
return
# 去除 debug
if line.character.begins_with(GlobalConfig.DEBUG_CHARACTER_PREFIX):
return
# 其他角色说话时会自动停止
if line.character == hook_character_name:
# 未在 speaking 状态
if _last_line_session < 0:
_last_line_session = hook_speaking()
if GlobalConfig.DEBUG:
print("[Npc2D] Hook speaking for character: ", hook_character_name, " line.character:", line.character)
elif _last_line_session >= 0:
if GlobalConfig.DEBUG:
print("[Npc2D] Unhook speaking for character: ", hook_character_name, " line.character:", line.character)
unhook_speaking(_last_line_session)
_last_line_session = -1
# dialog 结束时会自动 unhook 从 line 播放的 speaking
func _on_dialogue_ended(_res) -> void:
unhook_speaking(_last_line_session)
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
2025-06-30 10:33:40 +00:00
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)
2025-06-06 16:19:37 +00:00
visibility_changed.connect(_on_visibility_changed)
# 开始动画
if sprite_frames and animation:
play()
2025-06-06 16:19:37 +00:00
2025-06-14 08:46:32 +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:
_align_signs_status()
2025-06-14 08:46:32 +00:00
func _align_signs_status() -> void:
if not is_node_ready():
return
2025-07-17 08:43:30 +00:00
if Engine.is_editor_hint():
_setup_editor_preview()
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
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:
# 退出强制播放模式
# 参数:
2025-07-17 08:43:30 +00:00
# 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:
2025-07-17 08:43:30 +00:00
# 返回当前是否处于强制播放状态
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]:
var hint_str = ""
if Engine.is_editor_hint() and dialogue_res:
hint_str = ",".join(dialogue_res.get_ordered_titles())
2025-01-10 09:53:12 +00:00
return [
{
"name": "dialogue_title",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_ENUM_SUGGESTION,
"hint_string": hint_str
2025-01-10 09:53:12 +00:00
}
]