feat(client): add interaction prompt system (#405)
Server-driven interaction prompt that displays "E - Talk" when near an interactable NPC. Decodes v4 nearby_interactions from snapshot, stores in GameState, renders via InteractionPrompt UI with fade animation. Extensible interface (get_interaction_target/get_selected_verb) for future radial verb menu (v0.2). - Protocol: decode nearby_interactions array with nested VerbOption structs, entity relationship/observation fields, tick_rate in GameTime - GameState: store/clear nearby_interactions per snapshot - SimBridge: test mode generates v4 format with structured verbs - InteractionPrompt: PanelContainer with fade in/out, polls GameState - Tests: 19 new test cases covering protocol, state, sim bridge, UI, encoding - Fixture assertions updated for v4 protocol version Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
extends PanelContainer
|
||||
|
||||
# Interaction prompt — displays available interaction verb for nearby entity.
|
||||
# Server-driven: shows when GameState.nearby_interactions is non-empty,
|
||||
# hides when empty. No game logic — pure display layer.
|
||||
#
|
||||
# v0.1: Single-line "E - Talk" (first verb on nearest entity)
|
||||
# v0.2: Will be replaced/extended with radial verb menu.
|
||||
# Public interface: get_interaction_target(), get_selected_verb()
|
||||
|
||||
@onready var prompt_label: Label = $MarginContainer/PromptLabel
|
||||
|
||||
var _is_showing: bool = false
|
||||
var _active_tween: Tween = null
|
||||
var _current_target_id: int = -1
|
||||
|
||||
const FADE_IN: float = 0.15
|
||||
const FADE_OUT: float = 0.15
|
||||
|
||||
func _ready() -> void:
|
||||
modulate.a = 0.0
|
||||
visible = false
|
||||
_is_showing = false
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var interactions: Array = GameState.nearby_interactions
|
||||
if interactions.size() > 0:
|
||||
_show_prompt(interactions[0])
|
||||
elif _is_showing:
|
||||
_hide_prompt()
|
||||
|
||||
func _show_prompt(interaction: Dictionary) -> void:
|
||||
var target_id: int = interaction.get("entity_id", -1)
|
||||
var verbs: Array = interaction.get("verbs", [])
|
||||
|
||||
if verbs.is_empty():
|
||||
if _is_showing:
|
||||
_hide_prompt()
|
||||
return
|
||||
|
||||
# v0.1: pick first verb (sorted by priority from server)
|
||||
var verb_label: String = verbs[0].get("label", "")
|
||||
var display_text: String = "E - %s" % verb_label
|
||||
|
||||
if _current_target_id != target_id or prompt_label.text != display_text:
|
||||
prompt_label.text = display_text
|
||||
_current_target_id = target_id
|
||||
|
||||
if not _is_showing:
|
||||
visible = true
|
||||
_is_showing = true
|
||||
if _active_tween and _active_tween.is_valid():
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(self, "modulate:a", 1.0, FADE_IN)
|
||||
|
||||
func _hide_prompt() -> void:
|
||||
if not _is_showing:
|
||||
return
|
||||
_is_showing = false
|
||||
_current_target_id = -1
|
||||
if _active_tween and _active_tween.is_valid():
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(self, "modulate:a", 0.0, FADE_OUT)
|
||||
_active_tween.tween_callback(func(): visible = false)
|
||||
|
||||
## Returns the current interaction target entity ID, or -1 if no interaction.
|
||||
func get_interaction_target() -> int:
|
||||
return _current_target_id
|
||||
|
||||
## Returns the selected verb kind (v0.1: first verb on nearest, v0.2: radial selection).
|
||||
func get_selected_verb() -> String:
|
||||
var interactions: Array = GameState.nearby_interactions
|
||||
if interactions.is_empty():
|
||||
return ""
|
||||
var verbs: Array = interactions[0].get("verbs", [])
|
||||
if verbs.is_empty():
|
||||
return ""
|
||||
return verbs[0].get("kind", "")
|
||||
@@ -0,0 +1 @@
|
||||
uid://b43jk3eu2uig
|
||||
@@ -0,0 +1,28 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/interaction_prompt.gd" id="1_prompt"]
|
||||
|
||||
[node name="InteractionPrompt" type="PanelContainer"]
|
||||
anchors_preset = 7
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 200.0
|
||||
offset_top = -60.0
|
||||
offset_right = -200.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_prompt")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 8
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 8
|
||||
|
||||
[node name="PromptLabel" type="Label" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
text = ""
|
||||
Reference in New Issue
Block a user