@tool class_name Npc2D extends AnimatedSprite2D signal interacted # <0 means no walk to edge @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(): 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(): 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 @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 @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): icount = val 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 # 0 hide; 1 silent; 2 speaking var speaking_sign_mode := 0: 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) 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") 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 if Engine.is_editor_hint(): # editor 下都显示 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 sign_mark.display_sign = true return # setup default value ground_archive = ArchiveManager.archive.ground_archive() icount = ground_archive.get_value(name, "icount", 0) if snap_to_edge: sign_snapper.arrived.connect(_on_interacted) else: sign_mark.interacted.connect(_on_interacted) # sign_mark.cancel.connect(_stop_speaking) sign_mark.toggle_active.connect(_on_toggle_active) visibility_changed.connect(_on_visibility_changed) if sprite_frames and animation: play() 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 _on_toggle_active(activated: bool) -> void: if activated and speaking_sign_mode == 0: speaking_sign_mode = 1 elif not activated and speaking_sign_mode > 0: speaking_sign_mode = 0 func _on_interacted() -> void: # play dialogue if dialogue_title: if GlobalConfig.DEBUG: print("[" + name + "] call lock") SceneManager.lock_player(0, action_key) icount += 1 ground_archive.set_pair(name, "icount", icount) DialogueManager.show_dialogue_balloon(dialogue_res, dialogue_title) interacted.emit() speaking_sign_mode = 2 await DialogueManager.dialogue_ended speaking_sign_mode = 1 if GlobalConfig.DEBUG: print("[" + name + "] call lock") SceneManager.unlock_player() 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(): hint_str = ",".join(dialogue_res.get_ordered_titles()) return [ { "name": "dialogue_title", "type": TYPE_STRING, "hint": PROPERTY_HINT_ENUM_SUGGESTION, "hint_string": hint_str } ]