Houses ~8.5 m; chimps capped ~0.85 m with safer axis fitting. Large GLB/PBR packs stay local until Git LFS (Forgejo HTTP 413). Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
4.1 KiB
GDScript
113 lines
4.1 KiB
GDScript
@tool
|
|
extends Node3D
|
|
class_name InteractableNPC
|
|
## Billboard NPC with dialog key and optional quest hook.
|
|
## @tool: applies texture/height in the editor so NPCs are WYSIWYG (no quest logic).
|
|
|
|
signal interacted(npc: InteractableNPC)
|
|
|
|
@export var dialog_key: String = "dialog_hongkong_npc"
|
|
@export var texture_path: String = "res://assets/sprites/placeholders/npc.png":
|
|
set(v):
|
|
texture_path = v
|
|
if Engine.is_editor_hint() and is_node_ready():
|
|
_apply_billboard_look()
|
|
@export var billboard_height: float = 1.74:
|
|
set(v):
|
|
billboard_height = v
|
|
if Engine.is_editor_hint() and is_node_ready():
|
|
_apply_billboard_look()
|
|
@export var billboard_max_width: float = 2.2:
|
|
set(v):
|
|
billboard_max_width = v
|
|
if Engine.is_editor_hint() and is_node_ready():
|
|
_apply_billboard_look()
|
|
@export var can_interact: bool = true ## false = deco only (e.g. village house)
|
|
@export var give_item_id: String = ""
|
|
@export var give_once: bool = false ## Don't stack if player already has the item
|
|
@export var despawn_after_give: bool = false ## Remove world pickup after taking
|
|
@export var require_item_id: String = ""
|
|
@export var consume_required: bool = false
|
|
@export var quest_flag: String = ""
|
|
@export var achievement_id: String = ""
|
|
@export var on_success_goto: String = ""
|
|
@export var prompt_key: String = "" ## Optional override, e.g. prompt_press_button
|
|
|
|
@onready var _billboard: BillboardSprite = $BillboardSprite
|
|
|
|
func _ready() -> void:
|
|
_apply_billboard_look()
|
|
if Engine.is_editor_hint():
|
|
return
|
|
if can_interact:
|
|
add_to_group("interactable")
|
|
|
|
|
|
func _apply_billboard_look() -> void:
|
|
if _billboard == null:
|
|
_billboard = get_node_or_null("BillboardSprite") as BillboardSprite
|
|
if _billboard == null:
|
|
return
|
|
_billboard.max_width = billboard_max_width
|
|
_billboard.set_target_height(billboard_height)
|
|
if texture_path != "" and ResourceLoader.exists(texture_path):
|
|
var tex: Texture2D = load(texture_path)
|
|
if tex:
|
|
_billboard.set_texture(tex)
|
|
|
|
func on_interact(_player: Node) -> void:
|
|
if Engine.is_editor_hint() or not can_interact:
|
|
return
|
|
interacted.emit(self)
|
|
# World buttons / portals: go immediately (optional short bark via dialog_key)
|
|
var ui := get_node_or_null("/root/UIRoot")
|
|
if dialog_key != "" and on_success_goto == "":
|
|
if ui and ui.has_method("show_dialog_key"):
|
|
ui.show_dialog_key(dialog_key)
|
|
elif ui and ui.has_method("show_dialog"):
|
|
ui.show_dialog(Locale.tr_key(dialog_key), 3.5, dialog_key)
|
|
if require_item_id != "" and not Inventory.has_item(require_item_id):
|
|
return
|
|
if require_item_id != "" and consume_required:
|
|
Inventory.remove_item(require_item_id, 1)
|
|
if give_item_id != "":
|
|
if not (give_once and Inventory.has_item(give_item_id)):
|
|
Inventory.add_item(give_item_id, 1)
|
|
if despawn_after_give:
|
|
call_deferred("queue_free")
|
|
if quest_flag != "":
|
|
GameState.set_flag(quest_flag, true)
|
|
if quest_flag == "house_built":
|
|
GameState.house_built = true
|
|
if achievement_id != "":
|
|
Achievements.unlock(achievement_id)
|
|
if on_success_goto != "":
|
|
# Optional one-liner before leave (non-blocking bark if UI supports it)
|
|
if dialog_key != "" and ui and ui.has_method("show_dialog_key"):
|
|
ui.show_dialog_key(dialog_key, 1.2, false)
|
|
GameState.goto_scene(on_success_goto)
|
|
|
|
func get_interact_prompt() -> String:
|
|
if prompt_key != "":
|
|
return Locale.tr_key(prompt_key)
|
|
if texture_path.ends_with("button.png"):
|
|
return Locale.tr_key("prompt_press_button")
|
|
if texture_path.ends_with("tiramisu.png"):
|
|
return Locale.tr_key("prompt_eat_tiramisu")
|
|
if give_item_id != "":
|
|
var item_name := Locale.tr_key("item_" + give_item_id)
|
|
return "%s — %s" % [Locale.tr_key("prompt_pick_up"), item_name]
|
|
if on_success_goto != "":
|
|
return Locale.tr_key("prompt_enter")
|
|
if quest_flag == "house_built":
|
|
return Locale.tr_key("prompt_build")
|
|
if dialog_key != "":
|
|
var label := Locale.tr_key(dialog_key)
|
|
if label.length() > 42:
|
|
label = label.substr(0, 40) + "…"
|
|
return "%s — %s" % [Locale.tr_key("prompt_talk"), label]
|
|
return Locale.tr_key("prompt_use")
|
|
|
|
func set_interaction_highlight(enabled: bool, color: Color = Color(1.4, 1.35, 0.4)) -> void:
|
|
if _billboard and _billboard.has_method("set_highlight"):
|
|
_billboard.set_highlight(enabled, color)
|