2026-01-15 12:04:52 +00:00
|
|
|
class_name Interactable extends Node3D
|
|
|
|
|
|
2026-01-15 12:39:56 +00:00
|
|
|
@export var interaction: PackedScene = null
|
|
|
|
|
var interaction_ui : Control = null
|
2026-01-15 12:04:52 +00:00
|
|
|
|
|
|
|
|
@onready var view: Node3D = $View
|
|
|
|
|
@onready var frame: Sprite3D = $Frame
|
2026-01-15 12:39:56 +00:00
|
|
|
@onready var canvas_layer: CanvasLayer = $CanvasLayer
|
2026-01-15 12:04:52 +00:00
|
|
|
|
2026-01-15 14:28:56 +00:00
|
|
|
@onready var caption : Label3D = %Caption
|
|
|
|
|
@onready var prompt : Label3D = %Prompt
|
|
|
|
|
|
2026-01-15 12:39:56 +00:00
|
|
|
var active : bool = true
|
2026-01-15 12:04:52 +00:00
|
|
|
var shown : bool = false
|
|
|
|
|
var hover : bool = false
|
2026-01-15 14:28:56 +00:00
|
|
|
var collected : bool = false:
|
|
|
|
|
set(value):
|
|
|
|
|
collected = value
|
|
|
|
|
if is_inside_tree():
|
|
|
|
|
_update_prompt()
|
2026-01-15 12:04:52 +00:00
|
|
|
|
|
|
|
|
var tween: Tween = null
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
view.scale = Vector3.ZERO
|
|
|
|
|
frame.modulate.a = 0
|
2026-01-15 12:39:56 +00:00
|
|
|
if interaction:
|
|
|
|
|
interaction_ui = interaction.instantiate() as Control
|
|
|
|
|
canvas_layer.add_child(interaction_ui)
|
2026-01-15 14:28:56 +00:00
|
|
|
_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()
|
2026-01-15 12:39:56 +00:00
|
|
|
Scenes.player_enable.connect(_player_active) # TODO: do I have to clean this up?
|
|
|
|
|
|
2026-01-15 14:28:56 +00:00
|
|
|
|
2026-01-15 12:39:56 +00:00
|
|
|
func _player_active(value: bool) -> void:
|
|
|
|
|
active = value
|
2026-01-15 12:04:52 +00:00
|
|
|
|
2026-01-15 14:49:35 +00:00
|
|
|
func expand() -> void:
|
2026-01-15 17:07:50 +00:00
|
|
|
shown = true
|
|
|
|
|
view.scale = Vector3.ZERO
|
|
|
|
|
frame.modulate = Color.TRANSPARENT
|
|
|
|
|
view.rotation.z = -PI*0.5 # Godot angle wrapping is ... something
|
|
|
|
|
if tween: tween.kill()
|
|
|
|
|
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(view, "rotation:z", 0, 0.8).set_delay(0.5)
|
|
|
|
|
tween.parallel().tween_property(frame, "modulate:a", 1.0, 2.0)
|
2026-01-15 14:49:35 +00:00
|
|
|
|
|
|
|
|
func collapse() -> void:
|
|
|
|
|
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.6)
|
2026-01-15 14:28:56 +00:00
|
|
|
|
2026-01-15 12:04:52 +00:00
|
|
|
func _process(_delta: float) -> void:
|
|
|
|
|
_process_billboard()
|
2026-01-15 17:07:50 +00:00
|
|
|
_process_hover()
|
2026-01-15 12:04:52 +00:00
|
|
|
|
|
|
|
|
func _process_billboard() -> void:
|
|
|
|
|
if shown:
|
|
|
|
|
var player_view := State.player_view
|
|
|
|
|
look_at(player_view.global_position, Vector3.UP, true)
|
|
|
|
|
|
|
|
|
|
func _process_hover() -> void:
|
2026-01-15 12:39:56 +00:00
|
|
|
if active and hover and not shown:
|
2026-01-15 14:49:35 +00:00
|
|
|
expand()
|
2026-01-15 12:04:52 +00:00
|
|
|
|
2026-01-15 12:39:56 +00:00
|
|
|
elif not hover and shown or shown and not active:
|
2026-01-15 14:49:35 +00:00
|
|
|
collapse()
|
2026-01-15 12:04:52 +00:00
|
|
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
2026-01-15 12:39:56 +00:00
|
|
|
if not active or not hover or not shown: return
|
|
|
|
|
|
2026-01-15 14:28:56 +00:00
|
|
|
var clicked : bool = (event.is_action_pressed("ui_accept")) or (event is InputEventMouseButton and event.pressed)
|
2026-01-15 12:39:56 +00:00
|
|
|
if hover and shown and clicked:
|
|
|
|
|
collect_memento()
|
|
|
|
|
|
|
|
|
|
|
2026-01-15 14:28:56 +00:00
|
|
|
func play_story() -> void:
|
2026-01-15 14:49:35 +00:00
|
|
|
collected = true
|
2026-01-15 12:39:56 +00:00
|
|
|
canvas_layer.show()
|
|
|
|
|
Scenes.begin_sequence(interaction_ui.scene_id)
|
|
|
|
|
|
|
|
|
|
# Play the story
|
|
|
|
|
await interaction_ui.play()
|
|
|
|
|
|
|
|
|
|
# Pick the cards
|
|
|
|
|
var picker := State.room.get_node("%Picker") as CardPicker
|
|
|
|
|
await picker.pick_cards(interaction_ui.scene_id, false)
|
|
|
|
|
|
|
|
|
|
# Hide the CanvasLayer when done
|
|
|
|
|
canvas_layer.hide()
|
|
|
|
|
Scenes.end_sequence(interaction_ui.scene_id) # todo: maybe later?
|
|
|
|
|
|
|
|
|
|
Scenes.player_enable.emit(true) # TODO: this may not be our job?
|
2026-01-15 14:49:35 +00:00
|
|
|
expand()
|
2026-01-15 14:28:56 +00:00
|
|
|
|
|
|
|
|
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)
|
2026-01-15 14:49:35 +00:00
|
|
|
expand()
|
2026-01-15 14:28:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func collect_memento() -> void:
|
|
|
|
|
shown = false
|
|
|
|
|
collected = true
|
2026-01-15 14:49:35 +00:00
|
|
|
collapse()
|
2026-01-15 14:28:56 +00:00
|
|
|
|
|
|
|
|
# Hide mouse and collapse other interactables BEFORE showing canvas
|
|
|
|
|
get_tree().call_group("interactables", "collapse")
|
|
|
|
|
|
|
|
|
|
# Show the CanvasLayer so the story is visible full-screen
|
|
|
|
|
canvas_layer.show()
|
|
|
|
|
|
|
|
|
|
if interaction_ui is StoryPlayable:
|
|
|
|
|
play_story()
|
2026-01-15 17:07:50 +00:00
|
|
|
|
2026-01-15 14:28:56 +00:00
|
|
|
if interaction_ui is CardBoard:
|
|
|
|
|
play_board()
|
|
|
|
|
|
|
|
|
|
## Updates caption label based on the instantiated interaction_ui
|
|
|
|
|
func _update_caption() -> void:
|
|
|
|
|
if interaction_ui is StoryPlayable:
|
|
|
|
|
var story := interaction_ui as StoryPlayable
|
|
|
|
|
match story.scene_id:
|
|
|
|
|
Scenes.id.YOUTH_DRAVEN:
|
|
|
|
|
caption.text = TranslationServer.translate("Starlight")
|
|
|
|
|
Scenes.id.YOUTH_CHILDHOOD:
|
|
|
|
|
caption.text = TranslationServer.translate("crafted Mask")
|
|
|
|
|
Scenes.id.YOUTH_VOICE_TRAINING:
|
|
|
|
|
caption.text = TranslationServer.translate("Comic Stash")
|
|
|
|
|
Scenes.id.YOUTH_JUI_JUTSU:
|
|
|
|
|
caption.text = TranslationServer.translate("Sports Clothes")
|
|
|
|
|
Scenes.id.TRANSITION:
|
|
|
|
|
caption.text = TranslationServer.translate("Move on")
|
|
|
|
|
Scenes.id.ADULT_DND:
|
|
|
|
|
caption.text = TranslationServer.translate("colorful Dice")
|
|
|
|
|
Scenes.id.ADULT_VOLUNTARY:
|
|
|
|
|
caption.text = TranslationServer.translate("Gemstone Art")
|
|
|
|
|
Scenes.id.ADULT_CHRISTMAS:
|
|
|
|
|
caption.text = TranslationServer.translate("Chat Messages")
|
|
|
|
|
Scenes.id.ADULT_EATING:
|
|
|
|
|
caption.text = TranslationServer.translate("Dishes")
|
|
|
|
|
Scenes.id.ADULT_UNI:
|
|
|
|
|
caption.text = TranslationServer.translate("Science Poster")
|
|
|
|
|
Scenes.id.ADULT_THERAPY:
|
|
|
|
|
caption.text = TranslationServer.translate("Doctors Note")
|
|
|
|
|
Scenes.id.ADULT_BURNOUT:
|
|
|
|
|
caption.text = TranslationServer.translate("Paperwork")
|
|
|
|
|
_:
|
|
|
|
|
caption.text = ""
|
|
|
|
|
elif interaction_ui is CardBoard:
|
|
|
|
|
caption.text = TranslationServer.translate("Mind Board")
|
|
|
|
|
|
|
|
|
|
## 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")
|