diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index da297226c..0a826303e 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=9 format=3 uid="uid://bswrmh7w8dbgm"] +[gd_scene load_steps=10 format=3 uid="uid://bswrmh7w8dbgm"] [ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] [ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"] @@ -8,6 +8,7 @@ [ext_resource type="PackedScene" path="res://ui/hud.tscn" id="6_hud"] [ext_resource type="PackedScene" path="res://ui/minimap.tscn" id="7_minimap"] [ext_resource type="PackedScene" path="res://ui/monologue_display.tscn" id="8_monologue"] +[ext_resource type="PackedScene" path="res://ui/interaction_prompt.tscn" id="9_prompt"] [node name="Game" type="Node2D"] script = ExtResource("1_main") @@ -36,3 +37,5 @@ zoom = Vector2(2, 2) [node name="Minimap" parent="UILayer" instance=ExtResource("7_minimap")] [node name="MonologueDisplay" parent="UILayer" instance=ExtResource("8_monologue")] + +[node name="InteractionPrompt" parent="UILayer" instance=ExtResource("9_prompt")] diff --git a/client/scripts/autoloads/game_state.gd b/client/scripts/autoloads/game_state.gd index c695e08e1..b8fde5743 100644 --- a/client/scripts/autoloads/game_state.gd +++ b/client/scripts/autoloads/game_state.gd @@ -19,6 +19,9 @@ var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral" # refined when the server assigns explicit player entity IDs). var player_entity_id: int = 1 +# v4 fields (#404/#405) +var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}] + func apply_snapshot(snapshot: Dictionary) -> void: current_snapshot = snapshot @@ -54,6 +57,12 @@ func apply_snapshot(snapshot: Dictionary) -> void: if snapshot.has("player_facing") and snapshot.player_facing is String: player_facing = snapshot.player_facing + # v4: nearby_interactions (#404/#405) + if snapshot.has("nearby_interactions") and snapshot.nearby_interactions is Array: + nearby_interactions = snapshot.nearby_interactions + else: + nearby_interactions = [] + # v2: visible_tiles with visibility sectors # Derives visible_positions when not explicitly provided (real server mode) if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0: diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index 98aad66b8..9e19a70c1 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -294,20 +294,34 @@ func _test_snapshot() -> Dictionary: "visibility": sector, }) + # v4: nearby_interactions when NPC is nearby and visible (#404/#405) + var nearby: Array = [] + if npc_dist <= 2 and _test_has_los(Vector2i(px, py), npc_pos): + nearby.append({ + "entity_id": 2, + "entity_type": "Npc", + "distance": npc_dist, + "verbs": [ + {"kind": "Talk", "label": "Talk", "priority": 0, "available": true}, + {"kind": "ExamineNpc", "label": "Examine", "priority": 1, "available": true}, + ], + }) + return { "tick": _test_tick, - "version": 2, + "version": 4, "game_time": { "day": 0, "time_of_day": _test_tick * 10, "day_phase": "Morning", - "paused": false, + "tick_rate": "Full", }, "player_facing": _test_facing, "entities": entities, "tiles": _test_tiles(), "visible_tiles": _test_visible_tiles(), "visible_positions": _test_visible_positions(), + "nearby_interactions": nearby, } # Generate a small test room: 8x6 room with walls, a door, and floor diff --git a/client/scripts/constants.gd.uid b/client/scripts/constants.gd.uid new file mode 100644 index 000000000..f0d00dd4a --- /dev/null +++ b/client/scripts/constants.gd.uid @@ -0,0 +1 @@ +uid://wmbu7ivynu5d diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 3d26cc0a3..f76dccd20 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -4,6 +4,7 @@ extends Node2D @onready var camera = $Camera2D @onready var hud = $UILayer/HUD @onready var monologue_display = $UILayer/MonologueDisplay +@onready var interaction_prompt = $UILayer/InteractionPrompt func _ready() -> void: print("The Settled Reach — client initialized") @@ -28,4 +29,12 @@ func _process(_delta: float) -> void: # Send queued input to simulation var inputs = InputMapper.flush_queue() for input in inputs: + # TODO: Once server accepts Interact(InteractData), attach target + verb: + # if input.action == InputMapper.Action.INTERACT: + # var target_id: int = interaction_prompt.get_interaction_target() + # if target_id >= 0: + # input["action_data"] = { + # "target_entity_id": target_id, + # "verb": interaction_prompt.get_selected_verb(), + # } SimBridge.send_input(input) diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index dad23ca0c..af8410f1a 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -70,6 +70,15 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: tile_entry["visibility"] = vis visible_tiles.append(tile_entry) + # v4: nearby_interactions (#404/#405) + var nearby_interactions: Array = [] + var raw_interactions: Variant = raw.get("nearby_interactions") + if raw_interactions is Array: + for raw_ni in raw_interactions: + var ni = _decode_nearby_interaction(raw_ni) + if ni != null: + nearby_interactions.append(ni) + return { "tick": tick, "entities": entities, @@ -78,6 +87,7 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: "game_time": game_time, "player_facing": player_facing, "visible_tiles": visible_tiles, + "nearby_interactions": nearby_interactions, } @@ -96,6 +106,19 @@ static func _decode_entity(raw: Dictionary) -> Variant: if raw_vis is String: visibility = raw_vis + # v4: relationship (D-033) and observation state + var relationship: String = "Unknown" + var raw_rel: Variant = raw.get("relationship") + if raw_rel is String: + relationship = raw_rel + + var observation: Variant = "Visible" + var raw_obs: Variant = raw.get("observation") + if raw_obs is String: + observation = raw_obs + elif raw_obs is Dictionary: + observation = _decode_enum_variant(raw_obs) + return { "entity_id": entity_id, "x": float(raw["x"]), @@ -103,6 +126,54 @@ static func _decode_entity(raw: Dictionary) -> Variant: "z": int(raw["z"]), "kind": _decode_enum_variant(raw["kind"]), "visibility": visibility, + "relationship": relationship, + "observation": observation, + } + + +## Decode a NearbyInteraction from a raw msgpack map. +## Returns {entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]} or null. +static func _decode_nearby_interaction(raw) -> Variant: + if not raw is Dictionary: + return null + if not raw.has("entity_id") or not raw.has("verbs"): + push_warning("Protocol: nearby_interaction missing required fields: %s" % str(raw.keys())) + return null + var verbs: Array[Dictionary] = [] + var raw_verbs: Variant = raw.get("verbs") + if raw_verbs is Array: + for rv in raw_verbs: + var verb = _decode_verb_option(rv) + if verb != null: + verbs.append(verb) + if verbs.is_empty(): + return null + var entity_type: String = "" + var raw_et: Variant = raw.get("entity_type") + if raw_et is String: + entity_type = raw_et + return { + "entity_id": int(raw["entity_id"]), + "entity_type": entity_type, + "distance": int(raw.get("distance", 0)), + "verbs": verbs, + } + + +## Decode a VerbOption from a raw msgpack map. +## Returns {kind, label, priority, available} or null. +static func _decode_verb_option(raw) -> Variant: + if not raw is Dictionary or not raw.has("label"): + return null + var kind: String = "" + var raw_kind: Variant = raw.get("kind") + if raw_kind is String: + kind = raw_kind + return { + "kind": kind, + "label": str(raw["label"]), + "priority": int(raw.get("priority", 0)), + "available": bool(raw.get("available", true)), } diff --git a/client/scripts/rendering/tile_renderer.gd.uid b/client/scripts/rendering/tile_renderer.gd.uid new file mode 100644 index 000000000..63f01eb5c --- /dev/null +++ b/client/scripts/rendering/tile_renderer.gd.uid @@ -0,0 +1 @@ +uid://d2evnkstnqtpb diff --git a/client/tests/test_e2e_connection.gd.uid b/client/tests/test_e2e_connection.gd.uid new file mode 100644 index 000000000..4ac961947 --- /dev/null +++ b/client/tests/test_e2e_connection.gd.uid @@ -0,0 +1 @@ +uid://bcny2nvx6b8q3 diff --git a/client/tests/test_interaction_prompt.gd b/client/tests/test_interaction_prompt.gd new file mode 100644 index 000000000..f3edd42d5 --- /dev/null +++ b/client/tests/test_interaction_prompt.gd @@ -0,0 +1,219 @@ +class_name TestInteractionPrompt +extends GdUnitTestSuite + +# Tests for ticket #405: Interaction prompt system. +# Covers protocol v4 decode, GameState storage, SimBridge test mode, +# InteractionPrompt UI, and input encoding. + + +# -- Protocol: v4 nearby_interactions decoding -- + +func test_protocol_decode_v4_with_nearby_interactions() -> void: + var raw := { + "tick": 10, + "version": 4, + "entities": [ + {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Player", + "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, + ], + "nearby_interactions": [{ + "entity_id": 2, + "entity_type": "Npc", + "distance": 1, + "verbs": [ + {"kind": "Talk", "label": "Talk", "priority": 0, "available": true}, + {"kind": "ExamineNpc", "label": "Examine", "priority": 1, "available": true}, + ], + }], + } + var encoded = Messagepack.encode(raw) + assert_that(encoded.status == null).is_true() + var snapshot = Protocol.decode_snapshot(encoded.value) + + assert_that(snapshot).is_not_null() + assert_that(snapshot.nearby_interactions.size()).is_equal(1) + var ni = snapshot.nearby_interactions[0] + assert_that(ni.entity_id).is_equal(2) + assert_that(ni.entity_type).is_equal("Npc") + assert_that(ni.distance).is_equal(1) + assert_that(ni.verbs.size()).is_equal(2) + assert_that(ni.verbs[0].kind).is_equal("Talk") + assert_that(ni.verbs[0].label).is_equal("Talk") + assert_that(ni.verbs[0].priority).is_equal(0) + assert_that(ni.verbs[0].available).is_true() + assert_that(ni.verbs[1].kind).is_equal("ExamineNpc") + +func test_protocol_decode_v2_no_nearby_interactions() -> void: + var raw := { + "tick": 5, + "version": 2, + "entities": [], + } + var encoded = Messagepack.encode(raw) + var snapshot = Protocol.decode_snapshot(encoded.value) + assert_that(snapshot).is_not_null() + assert_that(snapshot.nearby_interactions.size()).is_equal(0) + +func test_protocol_decode_empty_nearby_interactions() -> void: + var raw := { + "tick": 1, + "entities": [], + "nearby_interactions": [], + } + var encoded = Messagepack.encode(raw) + var snapshot = Protocol.decode_snapshot(encoded.value) + assert_that(snapshot.nearby_interactions.size()).is_equal(0) + +func test_protocol_decode_interaction_missing_verbs() -> void: + var raw := { + "tick": 1, + "entities": [], + "nearby_interactions": [{"entity_id": 2}], + } + var encoded = Messagepack.encode(raw) + var snapshot = Protocol.decode_snapshot(encoded.value) + assert_that(snapshot.nearby_interactions.size()).is_equal(0) + +func test_protocol_decode_interaction_empty_verbs() -> void: + var raw := { + "tick": 1, + "entities": [], + "nearby_interactions": [{"entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": []}], + } + var encoded = Messagepack.encode(raw) + var snapshot = Protocol.decode_snapshot(encoded.value) + assert_that(snapshot.nearby_interactions.size()).is_equal(0) + +func test_protocol_decode_v4_entity_relationship() -> void: + var raw := { + "tick": 1, + "version": 4, + "entities": [ + {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Npc", + "visibility": "Forward", "relationship": "Friendly", "observation": "Visible"}, + ], + } + var encoded = Messagepack.encode(raw) + var snapshot = Protocol.decode_snapshot(encoded.value) + assert_that(snapshot.entities[0].relationship).is_equal("Friendly") + + +# -- GameState: nearby_interactions storage -- + +func test_game_state_stores_nearby_interactions() -> void: + var ni := [{"entity_id": 2, "entity_type": "Npc", "distance": 1, + "verbs": [{"kind": "Talk", "label": "Talk", "priority": 0, "available": true}]}] + GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": ni}) + assert_that(GameState.nearby_interactions.size()).is_equal(1) + assert_that(GameState.nearby_interactions[0].entity_id).is_equal(2) + GameState.nearby_interactions = [] + +func test_game_state_clears_nearby_interactions_when_absent() -> void: + var ni := [{"entity_id": 2, "entity_type": "Npc", "distance": 1, + "verbs": [{"kind": "Talk", "label": "Talk", "priority": 0, "available": true}]}] + GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": ni}) + assert_that(GameState.nearby_interactions.size()).is_equal(1) + GameState.apply_snapshot({"tick": 2, "entities": []}) + assert_that(GameState.nearby_interactions.size()).is_equal(0) + + +# -- SimBridge test mode: nearby_interactions generation -- + +func test_sim_bridge_test_snapshot_interaction_when_near_npc() -> void: + SimBridge.reset_test_state() + SimBridge._test_player_pos = Vector2i(11, 9) + var snap = SimBridge._test_snapshot() + assert_that(snap.has("nearby_interactions")).is_true() + assert_that(snap.nearby_interactions.size()).is_equal(1) + assert_that(snap.nearby_interactions[0].entity_id).is_equal(2) + assert_that(snap.nearby_interactions[0].verbs.size()).is_greater(0) + +func test_sim_bridge_test_snapshot_no_interaction_when_far() -> void: + SimBridge.reset_test_state() + SimBridge._test_player_pos = Vector2i(10, 10) + var snap = SimBridge._test_snapshot() + assert_that(snap.nearby_interactions.size()).is_equal(0) + +func test_sim_bridge_test_snapshot_interaction_at_range_2() -> void: + SimBridge.reset_test_state() + SimBridge._test_player_pos = Vector2i(10, 9) + var snap = SimBridge._test_snapshot() + assert_that(snap.nearby_interactions.size()).is_equal(1) + +func test_sim_bridge_test_snapshot_v4_version() -> void: + SimBridge.reset_test_state() + var snap = SimBridge._test_snapshot() + assert_that(snap.version).is_equal(4) + + +# -- InteractionPrompt UI -- + +func test_prompt_get_selected_verb_returns_first_kind() -> void: + GameState.nearby_interactions = [{ + "entity_id": 2, "entity_type": "Npc", "distance": 1, + "verbs": [ + {"kind": "Talk", "label": "Talk", "priority": 0, "available": true}, + {"kind": "ExamineNpc", "label": "Examine", "priority": 1, "available": true}, + ], + }] + var prompt = _make_prompt() + assert_that(prompt.get_selected_verb()).is_equal("Talk") + prompt.queue_free() + GameState.nearby_interactions = [] + +func test_prompt_get_selected_verb_empty_when_no_interactions() -> void: + GameState.nearby_interactions = [] + var prompt = _make_prompt() + assert_that(prompt.get_selected_verb()).is_equal("") + prompt.queue_free() + +func test_prompt_get_target_negative_when_no_interactions() -> void: + GameState.nearby_interactions = [] + var prompt = _make_prompt() + assert_that(prompt.get_interaction_target()).is_equal(-1) + prompt.queue_free() + +func test_prompt_hidden_initially() -> void: + GameState.nearby_interactions = [] + var prompt = _make_prompt() + assert_that(prompt._is_showing).is_false() + prompt.queue_free() + + +# -- Input encoding: Interact -- + +func test_interact_encodes_as_unit_variant() -> void: + var inputs: Array = [{"tick": 100, "action_name": "Interact"}] + var bytes := Protocol.encode_player_inputs(inputs) + var raw = Messagepack.decode(bytes) + assert_that(raw.status == null).is_true() + assert_that(raw.value[0]["action"]).is_equal("Interact") + +func test_interact_with_data_encodes_as_data_variant() -> void: + # Future: once server accepts Interact(InteractData) + var inputs: Array = [{ + "tick": 100, + "action_name": "Interact", + "action_data": {"target_entity_id": 2, "verb": "Talk"}, + }] + var bytes := Protocol.encode_player_inputs(inputs) + var raw = Messagepack.decode(bytes) + assert_that(raw.status == null).is_true() + assert_that(raw.value[0]["action"] is Dictionary).is_true() + assert_that(raw.value[0]["action"].has("Interact")).is_true() + + +# -- Helpers -- + +func _make_prompt() -> PanelContainer: + var PromptScript = load("res://ui/interaction_prompt.gd") + var panel = PanelContainer.new() + panel.set_script(PromptScript) + var margin = MarginContainer.new() + margin.name = "MarginContainer" + panel.add_child(margin) + var label = Label.new() + label.name = "PromptLabel" + margin.add_child(label) + add_child(panel) + return panel diff --git a/client/tests/test_interaction_prompt.gd.uid b/client/tests/test_interaction_prompt.gd.uid new file mode 100644 index 000000000..7b6fc5f1b --- /dev/null +++ b/client/tests/test_interaction_prompt.gd.uid @@ -0,0 +1 @@ +uid://bu0ib6ucyleoc diff --git a/client/tests/test_protocol.gd b/client/tests/test_protocol.gd index 818f7659c..172e1be10 100644 --- a/client/tests/test_protocol.gd +++ b/client/tests/test_protocol.gd @@ -279,13 +279,12 @@ func test_decode_snapshot_v2_full() -> void: assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(500) - assert_that(snapshot.version).is_equal(2) + assert_that(snapshot.version).is_equal(4) # game_time assert_that(snapshot.game_time).is_not_null() assert_that(snapshot.game_time.day).is_equal(1) assert_that(snapshot.game_time.time_of_day).is_equal(720) - assert_that(snapshot.game_time.paused).is_false() # player_facing (unit enum → bare string) assert_that(snapshot.player_facing).is_equal("Southeast") @@ -302,12 +301,12 @@ func test_decode_snapshot_v2_full() -> void: func test_existing_fixtures_have_v2_fields() -> void: - # All fixtures are generated by v2 fixture_snapshot() — verify decoder extracts v2 fields + # All fixtures are generated by fixture_snapshot() — verify decoder extracts v2+ fields for fixture_name in ["snapshot_one_npc", "snapshot_empty", "snapshot_player", "snapshot_multi_entity"]: var bytes = _load_fixture(fixture_name) var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() - assert_that(snapshot.version).is_equal(2) + assert_that(snapshot.version).is_equal(4) assert_that(snapshot.player_facing).is_equal("North") assert_that(snapshot.game_time).is_not_null() diff --git a/client/tests/test_rendering.gd b/client/tests/test_rendering.gd index f2d354ae4..125a4ab7c 100644 --- a/client/tests/test_rendering.gd +++ b/client/tests/test_rendering.gd @@ -175,7 +175,7 @@ func test_sim_bridge_test_snapshot_has_v2_fields() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.has("version")).is_true() - assert_that(snap.version).is_equal(2) + assert_that(snap.version).is_equal(4) assert_that(snap.has("game_time")).is_true() assert_that(snap.has("player_facing")).is_true() assert_that(snap.has("visible_tiles")).is_true() diff --git a/client/tests/test_rendering.gd.uid b/client/tests/test_rendering.gd.uid new file mode 100644 index 000000000..4a8ac0423 --- /dev/null +++ b/client/tests/test_rendering.gd.uid @@ -0,0 +1 @@ +uid://cwyma3isr20u0 diff --git a/client/tests/test_sprint2_proof.gd.uid b/client/tests/test_sprint2_proof.gd.uid new file mode 100644 index 000000000..84fd263dd --- /dev/null +++ b/client/tests/test_sprint2_proof.gd.uid @@ -0,0 +1 @@ +uid://dn7p6lsej3702 diff --git a/client/ui/interaction_prompt.gd b/client/ui/interaction_prompt.gd new file mode 100644 index 000000000..dfa569d0c --- /dev/null +++ b/client/ui/interaction_prompt.gd @@ -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", "") diff --git a/client/ui/interaction_prompt.gd.uid b/client/ui/interaction_prompt.gd.uid new file mode 100644 index 000000000..ebd01b47c --- /dev/null +++ b/client/ui/interaction_prompt.gd.uid @@ -0,0 +1 @@ +uid://b43jk3eu2uig diff --git a/client/ui/interaction_prompt.tscn b/client/ui/interaction_prompt.tscn new file mode 100644 index 000000000..3ed02c5a0 --- /dev/null +++ b/client/ui/interaction_prompt.tscn @@ -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 = ""