class_name Interactable extends Area3D @export var interaction: PackedScene = null var playable : Playable = null @onready var view: Node3D = $View @onready var frame: Sprite3D = $Frame @onready var light: OmniLight3D = $OmniLight3D @onready var canvas_layer: CanvasLayer = $CanvasLayer @onready var note: Node3D = $View/Sprite3D @onready var caption : Label3D = %Caption @onready var prompt : Label3D = %Prompt @onready var original_light_energy : float = light.light_energy @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.0 light.visible = false Scenes.player_enable.connect(_player_active) # TODO: do I have to clean this up? ## To be called by room func pull_save_state() -> void: if interaction: playable = interaction.instantiate() as Control canvas_layer.add_child(playable) _update_caption() # Check if this scene was already completed (for re-entering rooms) if playable is StoryPlayable: var story := playable as StoryPlayable collected = Scenes.is_sequence_repeating(story.scene_id) else: _update_prompt() func _player_active(value: bool) -> void: active = value func expand() -> void: shown = true light.visible = true light.light_energy = 0 if tween: 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) tween.parallel().tween_property(light, "light_energy", original_light_energy, 1.0).set_trans(Tween.TRANS_QUART) func collapse() -> void: if not shown: return shown = false if tween: 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) tween.parallel().tween_property(light, "light_energy", 0, 1.0).set_trans(Tween.TRANS_QUAD) await tween.finished light.visible = false func _process(_delta: float) -> void: _process_billboard() _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 handle_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 Scenes.begin_sequence(playable.scene_id, repeat) # Allow room to prepare for scene (e.g., play animations) await State.room.prepare_scene_start(playable.scene_id, repeat) canvas_layer.show() await playable.appear() # Play the story await playable.play() # Pick the cards if not already picked if not repeat: var picker := State.room.get_node("%Picker") as CardPicker await picker.pick_cards(playable.scene_id) # Hide the CanvasLayer when done playable.vanish() canvas_layer.hide() Scenes.end_sequence(playable.scene_id, repeat) # todo: maybe later? expand() func play_board() -> void: canvas_layer.show() await playable.appear() # Play the board (handles mouse visibility and waits for close) await playable.play() await playable.vanish() canvas_layer.hide() Scenes.player_enable.emit(true) expand() func play_burner() -> void: canvas_layer.show() await playable.appear() # Play the burner mini-game, will actually send us to the next map await playable.play() await playable.vanish() canvas_layer.hide() func interact() -> void: Scenes.player_enable.emit(false) # we must wait for our own collapse, so it doesnt change its caption while the canvas shows await collapse() get_tree().call_group("interactables", "collapse") if playable is StoryPlayable: await play_story() if playable is CardBoard: await play_board() if playable is CardBurner: await play_burner() # player is re-enabled by the inner code, or the room proceeds to next scene ## Updates caption label based on the instantiated interaction_ui func _update_caption() -> void: if playable is StoryPlayable: var story := playable as StoryPlayable caption.text = I18n.get_story_caption(story.scene_id) if playable is CardBoard: caption.text = TranslationServer.translate("Mind Board") if playable is CardBurner: caption.text = TranslationServer.translate("leave") ## Updates prompt label based on the interaction type and collected state func _update_prompt() -> void: if playable is StoryPlayable: if collected: prompt.text = TranslationServer.translate("read again") else: prompt.text = TranslationServer.translate("MementoLabel_collect") elif playable is CardBoard: prompt.text = TranslationServer.translate("find connections")