diff --git a/scene/entity/npc.gd b/scene/entity/npc.gd index 1e57d9ec..ff40e767 100644 --- a/scene/entity/npc.gd +++ b/scene/entity/npc.gd @@ -2,10 +2,15 @@ class_name Npc2D extends AnimatedSprite2D signal interacted -# 在 unlock player 之前发射 -signal talk_finished +signal talk_finished # 在 unlock player 之前发射 -# <0 means no walk to edge +# 常量定义 +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 @@ -14,41 +19,47 @@ signal talk_finished 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(): + 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(): + 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(): - speaking_sign.position.x = sign_x_offset - sign_mark.position.x = sign_x_offset + _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(): - var shape = area2d.get_node("CollisionShape2D").shape - shape.size.x = collision_width_and_x.x - area2d.position.x = collision_width_and_x.y + _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 - ground_archive.set_pair(name, "icount", val) + if ground_archive: + ground_archive.set_pair(name, "icount", val) _align_signs_status() var dialogue_title := "" @@ -56,105 +67,246 @@ 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 - -# 0 hide; 1 silent; 2 speaking -var speaking_sign_mode := 0: +var speaking_sign_mode := SpeakingSignMode.HIDDEN: set(val): - if speaking_sign_mode != val: - speaking_sign_mode = val - if speaking_sign_tween and speaking_sign_tween.is_valid(): - speaking_sign_tween.kill() - speaking_sign_tween = create_tween() - if val == 0: - speaking_sign_tween.tween_property(speaking_sign, "modulate:a", 0.0, 0.3) - speaking_animation.stop() - elif val == 1: - speaking_sign_tween.tween_property(speaking_sign, "modulate", base_mod, 0.3) - speaking_sign_tween.parallel().tween_property( - speaking_sign, "scale", base_scale, 0.3 - ) - speaking_animation.play("speaking") - elif val == 2: - speaking_sign_tween.tween_property(speaking_sign, "modulate", Color.WHITE, 0.3) - speaking_sign_tween.parallel().tween_property( - speaking_sign, "scale", base_scale * 1.3, 0.3 - ) - speaking_animation.play("speaking") + 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: - # sign position - sign_mark.sign_mark_offset.y = -sign_mark_height - speaking_sign.position.y = -speaking_sign_height - sign_mark.position.x = sign_x_offset - speaking_sign.position.x = sign_x_offset - # collisiong shape - var shape = area2d.get_node("CollisionShape2D").shape - shape.size.x = collision_width_and_x.x - area2d.position.x = collision_width_and_x.y - sign_snapper.action_on_arrived = action_key - sign_snapper.radius = walk_to_edge_width - sign_snapper.enabled = snap_to_edge - # 设置 speaking_sign 默认值 - base_scale = speaking_sign.scale - base_mod = speaking_sign.modulate - speaking_sign.modulate.a = 0.0 + _initialize_components() + if Engine.is_editor_hint(): - # editor 下都显示 + _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 - speaking_sign.get_node("Sprite2D").position.x = -60.0 - speaking_sign.get_node("Sprite2D").frame = 2 + 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 - return - # setup default value + + +func _setup_game_mode() -> void: + # 获取存档数据 ground_archive = ArchiveManager.archive.ground_archive() icount = ground_archive.get_value(name, "icount", 0) - if snap_to_edge: + + # 连接信号 + if snap_to_edge and sign_snapper: sign_snapper.arrived.connect(_on_interacted) - else: + elif sign_mark: sign_mark.interacted.connect(_on_interacted) - # sign_mark.cancel.connect(_stop_speaking) - sign_mark.toggle_active.connect(_on_toggle_active) + + 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(): - sign_mark.enabled = enabled and is_visible_in_tree() - sign_mark.display_sign = icount == 0 - speaking_sign.visible = enabled and icount > 0 +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 = 0 - elif speaking_sign_mode == 0: - speaking_sign_mode = 1 + speaking_sign_mode = SpeakingSignMode.HIDDEN + elif speaking_sign_mode == SpeakingSignMode.HIDDEN: + speaking_sign_mode = SpeakingSignMode.SILENT func _on_interacted() -> void: - # play dialogue - if dialogue_title: - SceneManager.lock_player(0, action_key) - icount += 1 + 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 out_of_range = speaking_sign_mode == 0 - speaking_sign_mode = 2 - await DialogueManager.dialogue_ended - speaking_sign_mode = 0 if out_of_range else 1 - # 在 unlock 之前发射 - talk_finished.emit() - SceneManager.unlock_player() + + 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: @@ -172,8 +324,9 @@ func _set(property: StringName, value: Variant) -> bool: func _get_property_list() -> Array[Dictionary]: var hint_str = "" - if Engine.is_editor_hint(): + if Engine.is_editor_hint() and dialogue_res: hint_str = ",".join(dialogue_res.get_ordered_titles()) + return [ { "name": "dialogue_title", diff --git a/scene/ground/scene/c01/s06_孤儿院长廊围墙.gd b/scene/ground/scene/c01/s06_孤儿院长廊围墙.gd index 8a7f65a3..5e5170de 100644 --- a/scene/ground/scene/c01/s06_孤儿院长廊围墙.gd +++ b/scene/ground/scene/c01/s06_孤儿院长廊围墙.gd @@ -94,8 +94,10 @@ func pre_game_intro(): var p = $"../DeployLayer/四小孩画鬼差的对话ambush/FocusPoint" camera.focus_node(p, 3.0) await Util.wait(2.0) + _hook_npc3_speaking() DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_四个小孩画鬼差的对话") await DialogueManager.dialogue_ended + _unhook_npc3_speaking() # 重置镜头 SceneManager.focus_player_and_reset_zoom(2.5) await Util.wait(2.5) @@ -123,14 +125,17 @@ func game_intro() -> void: # DialogueManager.show_dialogue_balloon( # dialogue_c01, "c01_s06_谈论鬼差与猫鼠游戏", [GlobalConfig.DIALOG_IGNORE_INPUT] # ) + _hook_npc3_speaking() DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_谈论鬼差与猫鼠游戏") - DialogueManager.dialogue_ended.connect(_game_counting_down, CONNECT_ONE_SHOT) + await DialogueManager.dialogue_ended + _game_counting_down() func _game_counting_down(_res = null): $"Sfx猫鼠游戏".play() DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_猫鼠游戏BGM开始") await DialogueManager.dialogue_ended + _unhook_npc3_speaking() # 重置镜头 SceneManager.focus_player_and_reset_zoom(2.5) SceneManager.release_player() @@ -145,6 +150,14 @@ func _game_counting_down(_res = null): cat.get_node("猫咪嘶吼音效").play() +func _hook_npc3_speaking(_res = null) -> void: + game_kid.get_node("Npc对话3").hook_speaking() + + +func _unhook_npc3_speaking(_res = null) -> void: + game_kid.get_node("Npc对话3").unhook_speaking() + + # 玩家与三个小孩的互动计数 func _on_talked(id: int): #talk count diff --git a/scene/ground/scene/c01/s07_书店外.gd b/scene/ground/scene/c01/s07_书店外.gd index de5287bc..b3266430 100644 --- a/scene/ground/scene/c01/s07_书店外.gd +++ b/scene/ground/scene/c01/s07_书店外.gd @@ -71,7 +71,7 @@ func _knock_door(): await Util.wait(2.2) $"敲门音效".play() await Util.wait(1.2) - var stream = preload("res://asset/audio/sfx/交互/序章/03_书店外黄昏_开门.ogg") + var stream = preload("uid://ehgd455wq8to") AudioManager.play_sfx(stream) @@ -95,3 +95,14 @@ func seller_interacted(): # 播放获得动画 SceneManager.enable_prop_item("prop_信碎片2") SceneManager.release_player() + + +func jiandu_dialog_triggered() -> void: + var jiandu = $"../DeployLayer/举碗小孩/Npc监督小孩" + jiandu.hook_speaking() + DialogueManager.dialogue_ended.connect(_on_jiandu_dialog_ended, CONNECT_ONE_SHOT) + + +func _on_jiandu_dialog_ended(_res) -> void: + var jiandu = $"../DeployLayer/举碗小孩/Npc监督小孩" + jiandu.unhook_speaking() diff --git a/scene/ground/scene/c01/s07_书店外.tscn b/scene/ground/scene/c01/s07_书店外.tscn index 055662ee..2ea2c4cb 100644 --- a/scene/ground/scene/c01/s07_书店外.tscn +++ b/scene/ground/scene/c01/s07_书店外.tscn @@ -203,12 +203,6 @@ sprite_frames = ExtResource("6_thm8f") animation = &"杂戏团黄昏-其余小孩" autoplay = "杂戏团黄昏-其余小孩" -[node name="Ambush监督小孩" parent="Ground/DeployLayer/其余小孩" instance=ExtResource("9_f61dl")] -position = Vector2(-688, 53) -cooldown_time = 0.1 -lock_player_on_playing_dialogue = false -hook_dialogue_title = "c01_s07_监督小孩吉祥话" - [node name="Npc吉祥话1" parent="Ground/DeployLayer/其余小孩" instance=ExtResource("6_fw22n")] position = Vector2(-44, 78) sign_mark_height = 23.0 @@ -237,6 +231,20 @@ autoplay = "杂戏团黄昏_举碗小孩" position = Vector2(6, 57) note_key = "c01_s07_钱碗" +[node name="Npc监督小孩" parent="Ground/DeployLayer/举碗小孩" instance=ExtResource("6_fw22n")] +position = Vector2(6, 72) +snap_to_edge = false +enabled = false +sign_mark_height = 11.0 +speaking_sign_height = 54.0 + +[node name="Ambush监督小孩" parent="Ground/DeployLayer/举碗小孩" instance=ExtResource("9_f61dl")] +position = Vector2(-825, 53) +cooldown_time = 0.1 +lock_player_on_playing_dialogue = false +hook_dialogue_title = "c01_s07_监督小孩吉祥话" +hook_method = "jiandu_dialog_triggered" + [node name="报童" parent="Ground/DeployLayer" index="10" instance=ExtResource("9_slaub")] position = Vector2(2080, 6) sprite_frames = ExtResource("6_thm8f")