@tool class_name Ambush2D extends Node2D signal triggered @export var enabled := true: set(val): enabled = val if is_node_ready(): sign_mark.enabled = val _check_sign_display() @export_enum("enter", "area_enter", "interact") var trigger_mode := "enter": set(val): trigger_mode = val if is_node_ready(): _check_sign_display() @export var one_shot := true # 首次进入 tree 就直接启用 @export var on_first_enter_tree := false @export var freeze_time := 5.0 var hook_animation = "" @export var lock_player_on_playing_dialogue = true @export_enum("c01", "c02", "c03", "c04", "c05") var hook_dialogue_res = "c01": set(val): hook_dialogue_res = val match val: "c01": dialogue_res = dialogue_c01 "c02": dialogue_res = dialogue_c02 "c03": dialogue_res = dialogue_c03 "c04": dialogue_res = dialogue_c04 "c05": dialogue_res = dialogue_c05 if is_node_ready() and Engine.is_editor_hint(): notify_property_list_changed() var hook_dialogue_title := "" var hook_method := "" var dialogue_c01 = preload("res://asset/dialogue/c01.dialogue") var dialogue_c02 = preload("res://asset/dialogue/c02.dialogue") var dialogue_c03 = preload("res://asset/dialogue/c03.dialogue") var dialogue_c04 = preload("res://asset/dialogue/c04.dialogue") var dialogue_c05 = preload("res://asset/dialogue/c05.dialogue") var dialogue_res = dialogue_c01 var played_time := 0.0 # var played := false: # set(val): # if played != val and ground_archive: # ground_archive.set_pair(name, "played", played) # played = val @onready var sign_mark := %Sign as Sign @onready var area := %Area2D as Area2D var ground_archive: GroundArchive var played: bool: set(val): played = val ground_archive.set_pair(name, "played", played) # Called when the node enters the scene tree for the first time. func _ready() -> void: _check_sign_display() if Engine.is_editor_hint(): var animation_player = _get_animation_player() # 更新 hook_animation 的可选项 if animation_player: animation_player.animation_libraries_updated.connect(notify_property_list_changed) return if played: if GlobalConfig.DEBUG: print("Ambush has played, name=", name) return if on_first_enter_tree: _entered(null) sign_mark.interacted.connect(_interacted) area.body_entered.connect(_entered) area.area_entered.connect(_area_entered) sign_mark.enabled = enabled # setup default value ground_archive = ArchiveManager.archive.ground_archive() played = ground_archive.get_value(name, "played", false) played_time = 0.0 func _check_sign_display(): sign_mark.display_sign = trigger_mode == "interact" and (not one_shot or not played) func _get_animation_player() -> AnimationPlayer: var node = get_parent() while node and not node is Ground2D: node = node.get_parent() if node is Ground2D: return node.get_node("AnimationPlayer") as AnimationPlayer return null var trigger_mutex = Mutex.new() func _interacted(): if enabled and trigger_mode == "interact": _do_trigger() func _entered(_body = null): if enabled and trigger_mode == "enter": _do_trigger() func _area_entered(_area = null): if enabled and trigger_mode == "area_enter": _do_trigger() func _do_trigger(): var time = Time.get_ticks_msec() # 确保只有一个线程进入该逻辑,因为有时 player 碰撞和首次进入 tree 都会触发该方法 if not trigger_mutex.try_lock(): print("Ambush trigger mutex lock fail, name=", name) return print("Ambush trigger mutex locked, name=", name) if not one_shot and freeze_time > 0: var time_left = freeze_time - (time - played_time) * 0.001 if time_left > 0: if GlobalConfig.DEBUG: print("Ambush freeze time not reached, time left=", time_left) trigger_mutex.unlock() return if one_shot and played: trigger_mutex.unlock() return played_time = time played = true trigger_mutex.unlock() # hook_animation if hook_animation: var animation_player = _get_animation_player() if animation_player: animation_player.play(hook_animation) # hook_dialogue if hook_dialogue_title: if lock_player_on_playing_dialogue: SceneManager.freeze_player(0.0) DialogueManager.show_dialogue_balloon(dialogue_res, hook_dialogue_title) DialogueManager.dialogue_ended.connect(_on_dialogue_ended, CONNECT_ONE_SHOT) if hook_method: var animation_player = _get_animation_player() if animation_player: animation_player.call(hook_method) triggered.emit() if GlobalConfig.DEBUG: print("ambush triggered! name=", name) _check_sign_display() func _on_dialogue_ended(_res): if GlobalConfig.DEBUG: print("Ambush dialogue ended") if lock_player_on_playing_dialogue: SceneManager.release_player() func _get(property: StringName) -> Variant: if property == "hook_dialogue_title": return hook_dialogue_title elif property == "hook_animation": return hook_animation elif property == "hook_method": return hook_method return null func _set(property: StringName, value: Variant) -> bool: if property == "hook_dialogue_title": hook_dialogue_title = value return true elif property == "hook_animation": hook_animation = value return true elif property == "hook_method": hook_method = value return true return false func _get_property_list() -> Array[Dictionary]: var hint_methods = "" var hint_animation = "" if Engine.is_editor_hint(): var animation_player = _get_animation_player() if animation_player: # 更新 hook_animation 的可选项 var animation_list = animation_player.get_animation_list() hint_animation = ",".join(animation_list) # 更新 hook_method 的可选项 var method_list = animation_player.get_method_list() var methods = [] for method in method_list: if ( # bit operation (method.flags == METHOD_FLAG_NORMAL) # and method.args.size() == method.default_args.size() and method.args.is_empty() and not method.name.begins_with("_") ): methods.append(method.name) hint_methods = ",".join(methods) var hint_dialogue_title = "" if Engine.is_editor_hint(): hint_dialogue_title = ",".join(dialogue_res.get_ordered_titles()) return [ { "name": "hook_dialogue_title", "type": TYPE_STRING, "hint": PROPERTY_HINT_ENUM_SUGGESTION, "hint_string": hint_dialogue_title }, { "name": "hook_animation", "type": TYPE_STRING, "hint": PROPERTY_HINT_ENUM_SUGGESTION, "hint_string": hint_animation }, { "name": "hook_method", "type": TYPE_STRING, "hint": PROPERTY_HINT_ENUM_SUGGESTION, "hint_string": hint_methods } ]