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