class_name Interactable extends Node3D @export var interaction: PackedScene = null var interaction_ui : Playable = null @onready var view: Node3D = $View @onready var frame: Sprite3D = $Frame @onready var canvas_layer: CanvasLayer = $CanvasLayer @onready var note: Node3D = $View/Sprite3D @onready var caption : Label3D = %Caption @onready var prompt : Label3D = %Prompt @export var billboard : bool = true var active : bool = false var shown : bool = false var hover : bool = false var collected : bool = false: set(value): collected = value if is_inside_tree(): _update_prompt() var tween: Tween = null func _ready() -> void: assert(note and frame and canvas_layer, "Interactable must have views and frame attached") view.scale = Vector3.ZERO frame.modulate.a = 0 if interaction: interaction_ui = interaction.instantiate() as Control canvas_layer.add_child(interaction_ui) _update_caption() # Check if this scene was already completed (for re-entering rooms) if interaction_ui is StoryPlayable: var story := interaction_ui as StoryPlayable collected = Scenes.is_sequence_repeating(story.scene_id) else: _update_prompt() Scenes.player_enable.connect(_player_active) # TODO: do I have to clean this up? func _player_active(value: bool) -> void: active = value func expand() -> void: shown = true _process_billboard() if tween and tween.is_valid(): tween.kill() else: view.scale = Vector3.ZERO note.rotation.z = -PI*0.5 # Godot angle wrapping is ... something frame.modulate = Color.TRANSPARENT frame.scale = Vector3(1.5, 1.5, 1.5) tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) tween.parallel().tween_property(view, "scale", Vector3.ONE, 1.0).set_delay(0.5) tween.parallel().tween_property(note, "rotation:z", 0, 0.8).set_delay(0.5) tween.parallel().tween_property(frame, "modulate:a", 1.0, 2.0).set_trans(Tween.TRANS_QUAD) tween.parallel().tween_property(frame, "scale", Vector3.ONE, 1.0).set_trans(Tween.TRANS_QUART) func collapse() -> void: if not shown: return #TODO: test shown = false if tween and tween.is_valid(): tween.kill() tween = create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK) tween.parallel().tween_property(view, "scale", Vector3.ZERO, 0.3) tween.parallel().tween_property(frame, "modulate:a", 0, 0.5).set_trans(Tween.TRANS_QUAD) tween.parallel().tween_property(frame, "scale", Vector3.ONE * 2.0, 1.0).set_trans(Tween.TRANS_QUAD) await tween.finished func _process(_delta: float) -> void: _process_hover() func _process_billboard() -> void: if billboard and shown: var player_view := State.player_view view.look_at(player_view.global_position, Vector3.UP, true) frame.look_at(player_view.global_position, Vector3.UP, true) func _process_hover() -> void: if active and hover and not shown: expand() elif not hover and shown or shown and not active: collapse() func _input(event: InputEvent) -> void: if not active or not hover or not shown: return var clicked : bool = (event.is_action_pressed("ui_accept")) or (event is InputEventMouseButton and event.is_pressed()) if hover and shown and clicked: interact() func play_story() -> void: # Check if this is a repeat playthrough var repeat := collected collected = true canvas_layer.show() # Allow room to prepare for scene (e.g., play animations) await State.room.prepare_scene_start(interaction_ui.scene_id, repeat) Scenes.begin_sequence(interaction_ui.scene_id) # Play the story await interaction_ui.play() # Pick the cards if not already picked if not repeat: var picker := State.room.get_node("%Picker") as CardPicker await picker.pick_cards(interaction_ui.scene_id) Scenes.end_sequence(interaction_ui.scene_id) # todo: maybe later? # Hide the CanvasLayer when done canvas_layer.hide() Scenes.player_enable.emit(true) expand() func play_board() -> void: canvas_layer.show() # Play the board (handles mouse visibility and waits for close) await interaction_ui.play() # Hide the CanvasLayer when done canvas_layer.hide() Scenes.player_enable.emit(true) expand() func play_burner() -> void: canvas_layer.show() # Play the board (handles mouse visibility and waits for close) await interaction_ui.play() # Hide the CanvasLayer when done canvas_layer.hide() func interact() -> void: Scenes.player_enable.emit(false) shown = false await collapse() collected = true # collapse other interactables BEFORE showing canvas get_tree().call_group("interactables", "collapse") if interaction_ui is StoryPlayable: await play_story() if interaction_ui is CardBoard: await play_board() if interaction_ui is CardBurner: await play_burner() ## Updates caption label based on the instantiated interaction_ui func _update_caption() -> void: if interaction_ui is StoryPlayable: var story := interaction_ui as StoryPlayable caption.text = I18n.get_story_caption(story.scene_id) if interaction_ui is CardBoard: caption.text = TranslationServer.translate("Mind Board") if interaction_ui is CardBurner: caption.text = TranslationServer.translate("leave") ## Updates prompt label based on the interaction type and collected state func _update_prompt() -> void: if interaction_ui is StoryPlayable: if collected: prompt.text = TranslationServer.translate("read again") else: prompt.text = TranslationServer.translate("MementoLabel_collect") elif interaction_ui is CardBoard: prompt.text = TranslationServer.translate("find connections")