frame-of-mind/src/logic-scenes/interactable/interactable.gd

209 lines
5.9 KiB
GDScript3
Raw Normal View History

class_name Interactable extends Area3D
@export var interaction: PackedScene = null
2026-01-18 20:01:20 +00:00
var playable : Playable = null
@onready var view: Node3D = $View
@onready var frame: Sprite3D = $Frame
2026-01-21 15:01:51 +00:00
@onready var light: OmniLight3D = $OmniLight3D
@onready var canvas_layer: CanvasLayer = $CanvasLayer
2026-01-16 11:06:13 +00:00
@onready var note: Node3D = $View/Sprite3D
@onready var caption : Label3D = %Caption
@onready var prompt : Label3D = %Prompt
2026-01-21 15:01:51 +00:00
@onready var original_light_energy : float = light.light_energy
2026-01-16 11:06:13 +00:00
2026-01-15 20:40:00 +00:00
@export var billboard : bool = true
2026-01-16 20:14:28 +00:00
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
2026-01-21 15:01:51 +00:00
light.visible = false
Scenes.player_enable.connect(_player_active) # TODO: do I have to clean this up?
_pull_save_state.call_deferred()
func _pull_save_state() -> void:
if interaction:
2026-01-18 20:01:20 +00:00
playable = interaction.instantiate() as Control
canvas_layer.add_child(playable)
_update_caption()
# Check if this scene was already completed (for re-entering rooms)
2026-01-18 20:01:20 +00:00
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
2026-01-15 14:49:35 +00:00
func expand() -> void:
shown = true
2026-01-21 15:01:51 +00:00
light.visible = true
light.light_energy = 0
2026-01-21 15:01:51 +00:00
if tween: tween.kill()
2026-01-16 11:06:13 +00:00
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)
2026-01-16 11:06:13 +00:00
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)
2026-01-21 15:01:51 +00:00
tween.parallel().tween_property(light, "light_energy", original_light_energy, 1.0).set_trans(Tween.TRANS_QUART)
2026-01-16 11:06:13 +00:00
2026-01-15 14:49:35 +00:00
func collapse() -> void:
if not shown: return
2026-01-15 14:49:35 +00:00
shown = false
2026-01-21 15:01:51 +00:00
if tween: tween.kill()
2026-01-15 14:49:35 +00:00
tween = create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK)
tween.parallel().tween_property(view, "scale", Vector3.ZERO, 0.3)
2026-01-16 11:06:13 +00:00
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)
2026-01-21 15:01:51 +00:00
tween.parallel().tween_property(light, "light_energy", 0, 1.0).set_trans(Tween.TRANS_QUAD)
await tween.finished
2026-01-21 15:01:51 +00:00
light.visible = false
func _process(_delta: float) -> void:
2026-01-19 13:14:44 +00:00
_process_billboard()
_process_hover()
func _process_billboard() -> void:
2026-01-15 20:40:00 +00:00
if billboard and shown:
var player_view := State.player_view
2026-01-15 20:40:00 +00:00
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:
2026-01-15 14:49:35 +00:00
expand()
elif not hover and shown or shown and not active:
2026-01-15 14:49:35 +00:00
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:
2026-01-16 22:00:00 +00:00
interact()
func play_story() -> void:
2026-01-16 22:00:00 +00:00
# Check if this is a repeat playthrough
var repeat := collected
2026-01-15 14:49:35 +00:00
collected = true
2026-01-18 20:53:19 +00:00
Scenes.begin_sequence(playable.scene_id, repeat)
# Allow room to prepare for scene (e.g., play animations)
2026-01-18 20:01:20 +00:00
await State.room.prepare_scene_start(playable.scene_id, repeat)
canvas_layer.show()
await playable.appear()
# Play the story
2026-01-18 20:01:20 +00:00
await playable.play()
# Pick the cards if not already picked
if not repeat:
var picker := State.room.get_node("%Picker") as CardPicker
2026-01-18 20:01:20 +00:00
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?
2026-01-15 14:49:35 +00:00
expand()
func play_board() -> void:
canvas_layer.show()
2026-01-18 20:53:19 +00:00
await playable.appear()
# Play the board (handles mouse visibility and waits for close)
2026-01-18 20:01:20 +00:00
await playable.play()
2026-01-18 20:53:19 +00:00
await playable.vanish()
canvas_layer.hide()
Scenes.player_enable.emit(true)
2026-01-15 14:49:35 +00:00
expand()
func play_burner() -> void:
canvas_layer.show()
2026-01-18 20:53:19 +00:00
await playable.appear()
2026-01-18 20:53:19 +00:00
# Play the burner mini-game, will actually send us to the next map
2026-01-18 20:01:20 +00:00
await playable.play()
2026-01-18 20:53:19 +00:00
await playable.vanish()
canvas_layer.hide()
2026-01-16 22:00:00 +00:00
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")
2026-01-18 20:01:20 +00:00
if playable is StoryPlayable:
await play_story()
2026-01-15 20:40:00 +00:00
2026-01-18 20:01:20 +00:00
if playable is CardBoard:
await play_board()
2026-01-18 20:01:20 +00:00
if playable is CardBurner:
await play_burner()
# player is re-enabled by the inner code, or the room proceeds to next scene
2026-01-18 20:01:20 +00:00
## Updates caption label based on the instantiated interaction_ui
func _update_caption() -> void:
2026-01-18 20:01:20 +00:00
if playable is StoryPlayable:
var story := playable as StoryPlayable
caption.text = I18n.get_story_caption(story.scene_id)
2026-01-18 20:01:20 +00:00
if playable is CardBoard:
caption.text = TranslationServer.translate("Mind Board")
2026-01-18 20:01:20 +00:00
if playable is CardBurner:
caption.text = TranslationServer.translate("leave")
## Updates prompt label based on the interaction type and collected state
func _update_prompt() -> void:
2026-01-18 20:01:20 +00:00
if playable is StoryPlayable:
if collected:
prompt.text = TranslationServer.translate("read again")
else:
prompt.text = TranslationServer.translate("MementoLabel_collect")
2026-01-18 20:01:20 +00:00
elif playable is CardBoard:
prompt.text = TranslationServer.translate("find connections")