147 lines
4.3 KiB
GDScript
147 lines
4.3 KiB
GDScript
class_name PlayerInteractionHud
|
|
extends Control
|
|
|
|
const FEEDBACK_SECONDS := 2.0
|
|
const PROBE_INTERVAL_SECONDS := 0.1
|
|
const ENTER_OFFSET := 7.0
|
|
const ACCENT_READY := Color(0.9, 0.63, 0.28, 0.95)
|
|
const ACCENT_SUCCESS := Color(0.55, 0.78, 0.38, 0.95)
|
|
const ACCENT_BLOCKED := Color(0.82, 0.4, 0.24, 0.95)
|
|
|
|
@export var player: Node
|
|
@export var simulation_manager: Node
|
|
|
|
@onready var accent: ColorRect = $Accent
|
|
@onready var action_label: Label = $Copy/Action
|
|
@onready var detail_label: Label = $Copy/Detail
|
|
|
|
var resting_position: Vector2
|
|
var motion_tween: Tween
|
|
var current_context_key := ""
|
|
var feedback_version := 0
|
|
var feedback_active := false
|
|
var feedback_succeeded := false
|
|
var _probe_elapsed := 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
resting_position = position
|
|
_reset_transient_state()
|
|
if player == null or not player.has_signal("interaction_feedback"):
|
|
push_error("PlayerInteractionHud: player interaction feedback is unavailable")
|
|
return
|
|
player.interaction_feedback.connect(_on_interaction_feedback)
|
|
if simulation_manager != null and simulation_manager.has_signal("state_restored"):
|
|
simulation_manager.state_restored.connect(_on_state_restored)
|
|
call_deferred("refresh_prompt", true)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if feedback_active:
|
|
return
|
|
_probe_elapsed += delta
|
|
if _probe_elapsed < PROBE_INTERVAL_SECONDS:
|
|
return
|
|
_probe_elapsed = fmod(_probe_elapsed, PROBE_INTERVAL_SECONDS)
|
|
refresh_prompt()
|
|
|
|
|
|
func refresh_prompt(force: bool = false) -> void:
|
|
if force:
|
|
_probe_elapsed = 0.0
|
|
if player == null or not player.has_method("get_interaction_context"):
|
|
_hide_prompt()
|
|
return
|
|
var context: PlayerInteractionResult = player.get_interaction_context()
|
|
if context == null:
|
|
if force or not current_context_key.is_empty():
|
|
current_context_key = ""
|
|
_hide_prompt()
|
|
return
|
|
var next_key := context.cache_key()
|
|
if not force and next_key == current_context_key:
|
|
return
|
|
current_context_key = next_key
|
|
feedback_succeeded = false
|
|
_present(
|
|
"E · %s" % context.prompt_text,
|
|
context.detail_text,
|
|
ACCENT_READY if context.is_available() else ACCENT_BLOCKED,
|
|
true
|
|
)
|
|
|
|
|
|
func _on_interaction_feedback(heading: String, message: String, succeeded: bool) -> void:
|
|
feedback_version += 1
|
|
var version := feedback_version
|
|
feedback_active = true
|
|
feedback_succeeded = succeeded
|
|
current_context_key = ""
|
|
_present(heading, message, ACCENT_SUCCESS if succeeded else ACCENT_BLOCKED, true)
|
|
_resume_prompt_after_feedback(version)
|
|
|
|
|
|
func _resume_prompt_after_feedback(version: int) -> void:
|
|
await get_tree().create_timer(FEEDBACK_SECONDS).timeout
|
|
if version != feedback_version or not is_inside_tree():
|
|
return
|
|
feedback_active = false
|
|
refresh_prompt(true)
|
|
|
|
|
|
func _on_state_restored() -> void:
|
|
_reset_transient_state()
|
|
refresh_prompt(true)
|
|
|
|
|
|
func _present(heading: String, detail: String, accent_color: Color, animate: bool) -> void:
|
|
if motion_tween != null:
|
|
motion_tween.kill()
|
|
action_label.text = heading
|
|
detail_label.text = detail
|
|
accent.color = accent_color
|
|
visible = true
|
|
accent.pivot_offset = accent.size * 0.5
|
|
if not animate:
|
|
position = resting_position
|
|
modulate.a = 1.0
|
|
accent.scale.x = 1.0
|
|
return
|
|
position = resting_position + Vector2(0.0, ENTER_OFFSET)
|
|
modulate.a = 0.0
|
|
accent.scale.x = 0.18
|
|
motion_tween = create_tween().set_parallel(true)
|
|
motion_tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
motion_tween.tween_property(self, "position", resting_position, 0.22)
|
|
motion_tween.tween_property(self, "modulate:a", 1.0, 0.18)
|
|
motion_tween.tween_property(accent, "scale:x", 1.0, 0.28)
|
|
|
|
|
|
func _hide_prompt() -> void:
|
|
if not visible:
|
|
return
|
|
if motion_tween != null:
|
|
motion_tween.kill()
|
|
motion_tween = create_tween().set_parallel(true)
|
|
motion_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
|
motion_tween.tween_property(self, "modulate:a", 0.0, 0.16)
|
|
motion_tween.tween_property(self, "position:y", resting_position.y - 3.0, 0.16)
|
|
var hiding_tween := motion_tween
|
|
await hiding_tween.finished
|
|
if motion_tween == hiding_tween and current_context_key.is_empty() and not feedback_active:
|
|
visible = false
|
|
|
|
|
|
func _reset_transient_state() -> void:
|
|
feedback_version += 1
|
|
feedback_active = false
|
|
feedback_succeeded = false
|
|
current_context_key = ""
|
|
if motion_tween != null:
|
|
motion_tween.kill()
|
|
visible = false
|
|
position = resting_position
|
|
modulate.a = 0.0
|
|
if accent != null:
|
|
accent.scale.x = 1.0
|