feat: add exact player context actions
This commit is contained in:
@@ -149,6 +149,21 @@ These collaborators are `RefCounted` rule services, not additional scene-tree
|
|||||||
managers. Further decomposition should follow measured pressure and a proven
|
managers. Further decomposition should follow measured pressure and a proven
|
||||||
gameplay consumer.
|
gameplay consumer.
|
||||||
|
|
||||||
|
## Player context-interaction contract
|
||||||
|
|
||||||
|
`Player.get_interaction_context()` derives one ephemeral
|
||||||
|
`PlayerInteractionResult` in the same priority order used by `E`: hungry
|
||||||
|
animal, usable finite resource, guard site, study site, then pantry. The result
|
||||||
|
contains the exact stable target ID, action, display name, prompt copy,
|
||||||
|
availability, and loaded target reference. `try_interact()` executes that
|
||||||
|
single resolved result rather than maintaining a second target map.
|
||||||
|
|
||||||
|
`PlayerInteractionHud` formats the result and transient outcome feedback. It
|
||||||
|
does not select targets, mutate simulation state, or enter the save record.
|
||||||
|
State restore clears active feedback and re-derives the prompt from current
|
||||||
|
simulation and loaded-world facts. The HUD is separate from development
|
||||||
|
overlays so the embodied action remains legible in cinematic mode.
|
||||||
|
|
||||||
## Active-position contract
|
## Active-position contract
|
||||||
|
|
||||||
NpcVisual emits active position changes after successful movement.
|
NpcVisual emits active position changes after successful movement.
|
||||||
|
|||||||
@@ -817,14 +817,20 @@ Completed:
|
|||||||
preserves two exact reservations through save/restore, and feeds only the
|
preserves two exact reservations through save/restore, and feeds only the
|
||||||
named target. Private shelter ownership remains loaded-world context; no
|
named target. Private shelter ownership remains loaded-world context; no
|
||||||
herd system, animal index, or schema bump was introduced.
|
herd system, animal index, or schema bump was introduced.
|
||||||
|
43. `Jajce Context Action 10`: the existing `E` interactions now derive one
|
||||||
|
exact ephemeral result shared by prompt and execution. A restrained,
|
||||||
|
cardless bottom-center HUD names the nearest hungry goat, shows real pantry
|
||||||
|
availability, reports success or blockage, remains visible in cinematic
|
||||||
|
mode, and also covers existing resource, pantry, guard, and study actions.
|
||||||
|
Restore discards transient feedback and re-derives context from live facts.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. Playtest the named pair and record one concrete gameplay or scaling need
|
1. Make NPC feeding visibly collect one real pantry food into the caretaker's
|
||||||
before extending the animal model.
|
existing inventory/carried-food presentation before delivery to the exact
|
||||||
2. Keep breeding, products, herd scheduling, animal spatial indexing, and
|
reserved goat.
|
||||||
active/abstract simulation LOD deferred unless that evidence requires one
|
2. Prove conservation, claim continuity, and save/restore during both travel
|
||||||
of them; otherwise return to the broader milestone sequence.
|
legs; keep herd, product, indexing, and LOD systems deferred.
|
||||||
|
|
||||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||||
mechanics. The next proof is a beautiful stage for the systems that already
|
mechanics. The next proof is a beautiful stage for the systems that already
|
||||||
|
|||||||
@@ -947,14 +947,28 @@ Schema-v11 restore preserves both claim pairings and checksum. Loaded routine
|
|||||||
sites now distinguish private shelter ownership from shared species context
|
sites now distinguish private shelter ownership from shared species context
|
||||||
without adding that authored-world fact to saved simulation state.
|
without adding that authored-world fact to saved simulation state.
|
||||||
|
|
||||||
The immediate next step is a playtest-and-measure gate, not a herd abstraction.
|
The named-pair playtest gate exposed and closed its first embodied-use gap.
|
||||||
Keep loaded-animal discovery linear and use the named pair to identify one
|
`PlayerInteractionResult` now derives one exact, ephemeral context from the
|
||||||
concrete gameplay need before adding breeding, products, herd scheduling,
|
same priority and stable target used by `E`: named animal care, finite-resource
|
||||||
animal indexing, or active/abstract animal LOD. If no such need appears, resume
|
harvest, pantry eating, guard work, or study. A cardless bottom-center prompt
|
||||||
the broader milestone sequence instead of adding animal framework by momentum.
|
shows the action and real availability, remains visible in cinematic mode, and
|
||||||
|
briefly reports success or blockage. Restore clears transient feedback and
|
||||||
|
re-derives the current prompt without adding saved state or drawing RNG.
|
||||||
|
|
||||||
|
The next bounded slice should make NPC animal care physically honest:
|
||||||
|
`village_pantry -> NPC inventory -> exact named goat`. A caretaker should keep
|
||||||
|
the same animal claim, collect one real food into the existing carried-food
|
||||||
|
presentation, and deliver it without duplication across contention or
|
||||||
|
mid-route restore. Keep breeding, products, herd scheduling, animal indexing,
|
||||||
|
and active/abstract animal LOD deferred.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
|
- `Jajce Context Action 10`: one ephemeral result now keeps the displayed `E`
|
||||||
|
prompt and executed stable target aligned across existing player
|
||||||
|
interactions. Named goat care shows exact pantry availability and transient
|
||||||
|
success/blockage, survives cinematic mode, and re-derives after restore
|
||||||
|
without persistent UI state.
|
||||||
- `Jajce Goat Pair 09`: named goat Zora adds a second independent animal
|
- `Jajce Goat Pair 09`: named goat Zora adds a second independent animal
|
||||||
record and private shelter while sharing Dunja's pasture. Stable nearest
|
record and private shelter while sharing Dunja's pasture. Stable nearest
|
||||||
targeting, simultaneous exact reservations, isolated feed mutation and
|
targeting, simultaneous exact reservations, isolated feed mutation and
|
||||||
|
|||||||
+15
-6
@@ -228,6 +228,10 @@ plugin content, not game architecture.
|
|||||||
- Pressing `E` near a berry bush or tree extracts its configured yield into the
|
- Pressing `E` near a berry bush or tree extracts its configured yield into the
|
||||||
village through the same `ResourceNode` contract used by NPCs.
|
village through the same `ResourceNode` contract used by NPCs.
|
||||||
- Guard, study, rest, and food interactions now use typed world sites.
|
- Guard, study, rest, and food interactions now use typed world sites.
|
||||||
|
- A cardless bottom-center `E` prompt derives the exact current animal,
|
||||||
|
resource, storage, or activity target and its availability. Named goat care
|
||||||
|
reports pantry cost plus transient success or blockage, remains in cinematic
|
||||||
|
mode, and never enters saved state.
|
||||||
- `Escape` releases captured mouse input.
|
- `Escape` releases captured mouse input.
|
||||||
|
|
||||||
### Village simulation
|
### Village simulation
|
||||||
@@ -478,16 +482,19 @@ than duplicating these responsibilities across additional manager nodes.
|
|||||||
|
|
||||||
These implement camera-relative character movement, physical interaction
|
These implement camera-relative character movement, physical interaction
|
||||||
proximity checks, ResourceNode harvesting, mouse capture, and the elevated
|
proximity checks, ResourceNode harvesting, mouse capture, and the elevated
|
||||||
follow camera.
|
follow camera. `PlayerInteractionResult` is the ephemeral boundary shared by
|
||||||
|
the current `E` prompt and execution, so UI copy names the exact stable target
|
||||||
|
that receives the command.
|
||||||
|
|
||||||
### `world/ui/ui.gd`
|
### `world/ui/ui.gd`
|
||||||
|
|
||||||
The UI subscribes to village, task, event, knowledge, relationship, and
|
The UI subscribes to village, task, event, knowledge, relationship, and
|
||||||
opportunity changes. It formats aggregate state and resolves known facts,
|
opportunity changes. It formats aggregate state and resolves known facts,
|
||||||
relationship causes, and opportunity lifecycle details through manager queries
|
relationship causes, and opportunity lifecycle details through manager queries
|
||||||
without owning or recomputing simulation facts. `village_whisper_hud.gd` is a
|
without owning or recomputing simulation facts. `village_whisper_hud.gd` and
|
||||||
separate transient player layer that formats the same authoritative signals and
|
`player_interaction_hud.gd` are separate transient player layers that format
|
||||||
remains independent of the development overlays.
|
authoritative signals or the current exact context and remain independent of
|
||||||
|
the development overlays.
|
||||||
|
|
||||||
## Current runtime flow
|
## Current runtime flow
|
||||||
|
|
||||||
@@ -1005,8 +1012,10 @@ Dunja's pasture, nearest loaded care targeting supports two simultaneous exact
|
|||||||
claims, and feeding one goat cannot mutate the other. World-schema v11
|
claims, and feeding one goat cannot mutate the other. World-schema v11
|
||||||
preserves both claim pairings without serializing shelter ownership. Do not
|
preserves both claim pairings without serializing shelter ownership. Do not
|
||||||
generalize animal discovery or introduce herd scheduling, products, breeding,
|
generalize animal discovery or introduce herd scheduling, products, breeding,
|
||||||
or active/abstract animal LOD until playtest or measurement exposes a concrete
|
or active/abstract animal LOD. The named-pair gate instead produced one
|
||||||
need.
|
Milestone 9 context-action result shared by prompt and execution, with exact
|
||||||
|
pantry feedback and no persistent UI state. The next animal-facing proof should
|
||||||
|
make NPC care visibly carry one real pantry food to its exact reserved goat.
|
||||||
|
|
||||||
The remaining simulation-garden target still aims for:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
[ext_resource type="Script" uid="uid://bivrsk5ukgnoo" path="res://world/demo/DemoController.gd" id="13_demo"]
|
[ext_resource type="Script" uid="uid://bivrsk5ukgnoo" path="res://world/demo/DemoController.gd" id="13_demo"]
|
||||||
[ext_resource type="PackedScene" path="res://player/PlayerVisual.tscn" id="14_player_visual"]
|
[ext_resource type="PackedScene" path="res://player/PlayerVisual.tscn" id="14_player_visual"]
|
||||||
[ext_resource type="Script" uid="uid://dwwxux24jc6yq" path="res://world/ui/village_whisper_hud.gd" id="15_whisper"]
|
[ext_resource type="Script" uid="uid://dwwxux24jc6yq" path="res://world/ui/village_whisper_hud.gd" id="15_whisper"]
|
||||||
|
[ext_resource type="Script" path="res://world/ui/player_interaction_hud.gd" id="16_interaction"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
||||||
radius = 0.4
|
radius = 0.4
|
||||||
@@ -127,6 +128,71 @@ offset_right = 940.0
|
|||||||
offset_bottom = 66.0
|
offset_bottom = 66.0
|
||||||
script = ExtResource("12_timedial")
|
script = ExtResource("12_timedial")
|
||||||
|
|
||||||
|
[node name="PlayerInteractionLayer" type="CanvasLayer" parent="."]
|
||||||
|
layer = 2
|
||||||
|
|
||||||
|
[node name="PlayerInteractionHud" type="Control" parent="PlayerInteractionLayer" node_paths=PackedStringArray("player", "simulation_manager")]
|
||||||
|
visible = false
|
||||||
|
anchors_preset = 7
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -290.0
|
||||||
|
offset_top = -108.0
|
||||||
|
offset_right = 290.0
|
||||||
|
offset_bottom = -42.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
mouse_filter = 2
|
||||||
|
script = ExtResource("16_interaction")
|
||||||
|
player = NodePath("../../Player")
|
||||||
|
simulation_manager = NodePath("../../SimulationManager")
|
||||||
|
|
||||||
|
[node name="Accent" type="ColorRect" parent="PlayerInteractionLayer/PlayerInteractionHud"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 10
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_bottom = 2.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
color = Color(0.9, 0.63, 0.28, 0.95)
|
||||||
|
|
||||||
|
[node name="Copy" type="VBoxContainer" parent="PlayerInteractionLayer/PlayerInteractionHud"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = 8.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_constants/separation = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Action" type="Label" parent="PlayerInteractionLayer/PlayerInteractionHud/Copy"]
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_colors/font_color = Color(0.98, 0.92, 0.79, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0.07, 0.045, 0.025, 0.94)
|
||||||
|
theme_override_colors/font_shadow_color = Color(0.02, 0.01, 0.005, 0.7)
|
||||||
|
theme_override_constants/outline_size = 7
|
||||||
|
theme_override_constants/shadow_offset_x = 1
|
||||||
|
theme_override_constants/shadow_offset_y = 2
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
text = "E · Interact"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Detail" type="Label" parent="PlayerInteractionLayer/PlayerInteractionHud/Copy"]
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_colors/font_color = Color(0.85, 0.77, 0.64, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0.07, 0.045, 0.025, 0.9)
|
||||||
|
theme_override_constants/outline_size = 5
|
||||||
|
theme_override_font_sizes/font_size = 13
|
||||||
|
text = "Context"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="VillageWhisperLayer" type="CanvasLayer" parent="."]
|
[node name="VillageWhisperLayer" type="CanvasLayer" parent="."]
|
||||||
layer = 3
|
layer = 3
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
class_name PlayerInteractionResult
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const KIND_ANIMAL := &"animal"
|
||||||
|
const KIND_RESOURCE := &"resource"
|
||||||
|
const KIND_PANTRY := &"pantry"
|
||||||
|
const KIND_GUARD := &"guard"
|
||||||
|
const KIND_STUDY := &"study"
|
||||||
|
|
||||||
|
var kind: StringName
|
||||||
|
var action_id: StringName
|
||||||
|
var target_id: StringName
|
||||||
|
var display_name: String
|
||||||
|
var prompt_text: String
|
||||||
|
var detail_text: String
|
||||||
|
var blocked_reason: String
|
||||||
|
var target_node: Node
|
||||||
|
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
interaction_kind: StringName,
|
||||||
|
interaction_action_id: StringName,
|
||||||
|
interaction_target_id: StringName,
|
||||||
|
interaction_display_name: String,
|
||||||
|
interaction_prompt_text: String,
|
||||||
|
interaction_detail_text: String,
|
||||||
|
interaction_target_node: Node,
|
||||||
|
interaction_blocked_reason := ""
|
||||||
|
) -> void:
|
||||||
|
kind = interaction_kind
|
||||||
|
action_id = interaction_action_id
|
||||||
|
target_id = interaction_target_id
|
||||||
|
display_name = interaction_display_name
|
||||||
|
prompt_text = interaction_prompt_text
|
||||||
|
detail_text = interaction_detail_text
|
||||||
|
target_node = interaction_target_node
|
||||||
|
blocked_reason = interaction_blocked_reason
|
||||||
|
|
||||||
|
|
||||||
|
func is_available() -> bool:
|
||||||
|
return blocked_reason.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func cache_key() -> String:
|
||||||
|
return (
|
||||||
|
"|"
|
||||||
|
. join(
|
||||||
|
[
|
||||||
|
String(kind),
|
||||||
|
String(action_id),
|
||||||
|
String(target_id),
|
||||||
|
prompt_text,
|
||||||
|
detail_text,
|
||||||
|
blocked_reason,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bw2ng0q0wm030
|
||||||
+185
-51
@@ -1,5 +1,7 @@
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
signal interaction_feedback(heading: String, message: String, succeeded: bool)
|
||||||
|
|
||||||
@export var move_speed := 7.0
|
@export var move_speed := 7.0
|
||||||
@export var acceleration := 18.0
|
@export var acceleration := 18.0
|
||||||
@export var rotation_speed := 12.0
|
@export var rotation_speed := 12.0
|
||||||
@@ -56,73 +58,205 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func try_interact() -> void:
|
func try_interact() -> void:
|
||||||
|
var context := get_interaction_context()
|
||||||
|
if context == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
match context.kind:
|
||||||
|
PlayerInteractionResult.KIND_ANIMAL:
|
||||||
|
_execute_animal_interaction(context)
|
||||||
|
PlayerInteractionResult.KIND_RESOURCE:
|
||||||
|
_execute_resource_interaction(context)
|
||||||
|
PlayerInteractionResult.KIND_GUARD:
|
||||||
|
simulation_manager.add_safety(3.0)
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"Village guarded", "Safety increased through real guard-post work.", true
|
||||||
|
)
|
||||||
|
PlayerInteractionResult.KIND_STUDY:
|
||||||
|
simulation_manager.add_knowledge(2.0)
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"Knowledge shared", "Village knowledge increased at the study desk.", true
|
||||||
|
)
|
||||||
|
PlayerInteractionResult.KIND_PANTRY:
|
||||||
|
_execute_pantry_interaction(context)
|
||||||
|
|
||||||
|
|
||||||
|
func get_interaction_context() -> PlayerInteractionResult:
|
||||||
if simulation_manager == null:
|
if simulation_manager == null:
|
||||||
return
|
return null
|
||||||
|
var animal := _find_feed_animal()
|
||||||
if try_feed_animal():
|
if animal != null:
|
||||||
return
|
return _build_animal_context(animal)
|
||||||
|
var resource := _find_resource_node()
|
||||||
if try_harvest_resource_node():
|
if resource != null:
|
||||||
return
|
return _build_resource_context(resource)
|
||||||
|
|
||||||
if is_near_activity_site(guard_site):
|
if is_near_activity_site(guard_site):
|
||||||
simulation_manager.add_safety(3.0)
|
return PlayerInteractionResult.new(
|
||||||
print("Player helped guard the village.")
|
PlayerInteractionResult.KIND_GUARD,
|
||||||
elif is_near_activity_site(study_site):
|
SimulationIds.ACTION_PATROL,
|
||||||
simulation_manager.add_knowledge(2.0)
|
guard_site.site_id,
|
||||||
print("Player shared knowledge.")
|
guard_site.display_name,
|
||||||
elif is_near_storage(pantry_storage):
|
"Help guard the village",
|
||||||
simulation_manager.eat_food(stomach_capacity_for_food)
|
"Work here · +3 safety",
|
||||||
print("Player ate food from the village supply.")
|
guard_site
|
||||||
|
)
|
||||||
|
if is_near_activity_site(study_site):
|
||||||
|
return PlayerInteractionResult.new(
|
||||||
|
PlayerInteractionResult.KIND_STUDY,
|
||||||
|
SimulationIds.ACTION_STUDY,
|
||||||
|
study_site.site_id,
|
||||||
|
study_site.display_name,
|
||||||
|
"Share knowledge",
|
||||||
|
"Work here · +2 knowledge",
|
||||||
|
study_site
|
||||||
|
)
|
||||||
|
if is_near_storage(pantry_storage):
|
||||||
|
return _build_pantry_context()
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
func try_feed_animal() -> bool:
|
func _find_feed_animal() -> AnimalNode:
|
||||||
if not ("animal_care" in simulation_manager) or simulation_manager.animal_care == null:
|
if not ("animal_care" in simulation_manager) or simulation_manager.animal_care == null:
|
||||||
push_error("Player: SimulationManager has no animal-care service")
|
push_error("Player: SimulationManager has no animal-care service")
|
||||||
return false
|
return null
|
||||||
var node: AnimalNode = simulation_manager.animal_care.find_node_for_player(
|
return simulation_manager.animal_care.find_node_for_player(global_position, interaction_range)
|
||||||
global_position, interaction_range
|
|
||||||
)
|
|
||||||
if node == null:
|
|
||||||
return false
|
|
||||||
if not simulation_manager.has_method("feed_animal"):
|
|
||||||
push_error("Player: SimulationManager cannot feed AnimalNodes")
|
|
||||||
return true
|
|
||||||
if simulation_manager.feed_animal(node.animal_id, -1):
|
|
||||||
print("Player fed %s one food from the village pantry." % node.display_name)
|
|
||||||
else:
|
|
||||||
print("%s is hungry, but the pantry has no food to spare." % node.display_name)
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func try_harvest_resource_node() -> bool:
|
func _find_resource_node() -> ResourceNode:
|
||||||
if not simulation_manager.has_method("find_resource_node_for_player"):
|
if not simulation_manager.has_method("find_resource_node_for_player"):
|
||||||
push_error("Player: SimulationManager cannot find ResourceNodes")
|
push_error("Player: SimulationManager cannot find ResourceNodes")
|
||||||
return false
|
return null
|
||||||
var node: ResourceNode = simulation_manager.find_resource_node_for_player(
|
return simulation_manager.find_resource_node_for_player(global_position, interaction_range)
|
||||||
global_position, interaction_range
|
|
||||||
|
|
||||||
|
func _build_animal_context(node: AnimalNode) -> PlayerInteractionResult:
|
||||||
|
var cost := _get_animal_feed_cost()
|
||||||
|
var pantry_food := _get_pantry_food()
|
||||||
|
var blocked_reason := ""
|
||||||
|
var detail := "%.0f pantry food · %.0f available" % [cost, pantry_food]
|
||||||
|
if pantry_food < cost:
|
||||||
|
if pantry_food <= 0.0:
|
||||||
|
blocked_reason = "The village pantry is empty."
|
||||||
|
detail = "Pantry empty · needs %.0f food" % cost
|
||||||
|
else:
|
||||||
|
blocked_reason = "The village pantry does not have enough food."
|
||||||
|
detail = "Needs %.0f pantry food · %.1f available" % [cost, pantry_food]
|
||||||
|
return PlayerInteractionResult.new(
|
||||||
|
PlayerInteractionResult.KIND_ANIMAL,
|
||||||
|
SimulationIds.ACTION_FEED_ANIMAL,
|
||||||
|
node.animal_id,
|
||||||
|
node.display_name,
|
||||||
|
"Feed %s" % node.display_name,
|
||||||
|
detail,
|
||||||
|
node,
|
||||||
|
blocked_reason
|
||||||
)
|
)
|
||||||
if node == null:
|
|
||||||
return false
|
|
||||||
|
|
||||||
if not node.is_enabled():
|
|
||||||
print("%s is unavailable." % node.node_id)
|
|
||||||
return true
|
|
||||||
|
|
||||||
if node.is_depleted():
|
func _build_resource_context(node: ResourceNode) -> PlayerInteractionResult:
|
||||||
print("%s is depleted." % node.node_id)
|
var definition := SimulationDefinitions.get_action(node.action_id)
|
||||||
return true
|
var prompt := (
|
||||||
|
definition.display_name if definition != null else String(node.action_id).capitalize()
|
||||||
|
)
|
||||||
|
var display_name := String(node.node_id).replace("_", " ").capitalize()
|
||||||
|
return PlayerInteractionResult.new(
|
||||||
|
PlayerInteractionResult.KIND_RESOURCE,
|
||||||
|
node.action_id,
|
||||||
|
node.node_id,
|
||||||
|
display_name,
|
||||||
|
prompt,
|
||||||
|
"%s · %.1f remaining" % [display_name, node.get_amount_remaining()],
|
||||||
|
node
|
||||||
|
)
|
||||||
|
|
||||||
if not simulation_manager.has_method("harvest_resource_node"):
|
|
||||||
push_error("Player: SimulationManager cannot harvest ResourceNodes")
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
func _build_pantry_context() -> PlayerInteractionResult:
|
||||||
|
var pantry_food := _get_pantry_food()
|
||||||
|
var blocked_reason := "" if pantry_food > 0.0 else "The village pantry is empty."
|
||||||
|
var detail := "%.0f food available" % pantry_food
|
||||||
|
if not blocked_reason.is_empty():
|
||||||
|
detail = "Pantry empty"
|
||||||
|
return PlayerInteractionResult.new(
|
||||||
|
PlayerInteractionResult.KIND_PANTRY,
|
||||||
|
SimulationIds.ACTION_EAT,
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
"Village pantry",
|
||||||
|
"Eat from the pantry",
|
||||||
|
detail,
|
||||||
|
pantry_storage,
|
||||||
|
blocked_reason
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _execute_animal_interaction(context: PlayerInteractionResult) -> void:
|
||||||
|
var node := context.target_node as AnimalNode
|
||||||
|
if node == null or not context.is_available():
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"%s is still hungry" % context.display_name,
|
||||||
|
(
|
||||||
|
context.blocked_reason
|
||||||
|
if not context.blocked_reason.is_empty()
|
||||||
|
else "Animal care is unavailable."
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not simulation_manager.has_method("feed_animal"):
|
||||||
|
push_error("Player: SimulationManager cannot feed AnimalNodes")
|
||||||
|
return
|
||||||
|
if simulation_manager.feed_animal(context.target_id, -1):
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"%s is fed" % context.display_name,
|
||||||
|
"%.0f food moved from the village pantry." % _get_animal_feed_cost(),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"%s is still hungry" % context.display_name, "Animal care could not be completed.", false
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _execute_resource_interaction(context: PlayerInteractionResult) -> void:
|
||||||
|
var node := context.target_node as ResourceNode
|
||||||
|
if node == null or not simulation_manager.has_method("harvest_resource_node"):
|
||||||
|
push_error("Player: SimulationManager cannot harvest the resolved ResourceNode")
|
||||||
|
return
|
||||||
var extracted: float = simulation_manager.harvest_resource_node(node)
|
var extracted: float = simulation_manager.harvest_resource_node(node)
|
||||||
if extracted > 0.0:
|
if extracted <= 0.0:
|
||||||
print("Player gathered %s %s from %s." % [extracted, node.resource_id, node.node_id])
|
interaction_feedback.emit(
|
||||||
else:
|
"%s is unavailable" % context.display_name, "Nothing could be gathered.", false
|
||||||
print("%s could not be harvested." % node.node_id)
|
)
|
||||||
return true
|
return
|
||||||
|
var destination := "pantry" if node.resource_id == SimulationIds.RESOURCE_FOOD else "woodpile"
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"%s gathered" % String(node.resource_id).capitalize(),
|
||||||
|
"%.1f moved to the village %s." % [extracted, destination],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _execute_pantry_interaction(context: PlayerInteractionResult) -> void:
|
||||||
|
if not context.is_available():
|
||||||
|
interaction_feedback.emit("The pantry is empty", context.blocked_reason, false)
|
||||||
|
return
|
||||||
|
var food_before := _get_pantry_food()
|
||||||
|
simulation_manager.eat_food(stomach_capacity_for_food)
|
||||||
|
var consumed := food_before - _get_pantry_food()
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"Ate from the pantry", "%.0f village food consumed." % consumed, consumed > 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_animal_feed_cost() -> float:
|
||||||
|
var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
|
||||||
|
return definition.completion_cost_amount if definition != null else 1.0
|
||||||
|
|
||||||
|
|
||||||
|
func _get_pantry_food() -> float:
|
||||||
|
if not simulation_manager.has_method("get_pantry"):
|
||||||
|
return 0.0
|
||||||
|
var pantry: StorageStateRecord = simulation_manager.get_pantry()
|
||||||
|
return pantry.get_amount(SimulationIds.RESOURCE_FOOD) if pantry != null else 0.0
|
||||||
|
|
||||||
|
|
||||||
func is_near_storage(storage_node: StorageNode) -> bool:
|
func is_near_storage(storage_node: StorageNode) -> bool:
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||||
|
root.add_child(main_scene)
|
||||||
|
await process_frame
|
||||||
|
var manager: Node = main_scene.get_node("SimulationManager")
|
||||||
|
manager.set_process(false)
|
||||||
|
var player := main_scene.get_node("Player")
|
||||||
|
player.set_physics_process(false)
|
||||||
|
for _frame in 3:
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
|
var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
|
||||||
|
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
||||||
|
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||||
|
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||||
|
var hud := main_scene.get_node_or_null("PlayerInteractionLayer/PlayerInteractionHud")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
dunja_node != null
|
||||||
|
and zora_node != null
|
||||||
|
and dunja_state != null
|
||||||
|
and zora_state != null
|
||||||
|
and hud != null
|
||||||
|
),
|
||||||
|
"The real main scene should expose both named goats and the player interaction HUD",
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
dunja_node == null
|
||||||
|
or zora_node == null
|
||||||
|
or dunja_state == null
|
||||||
|
or zora_state == null
|
||||||
|
or hud == null
|
||||||
|
):
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
|
||||||
|
dunja_node.set_physics_process(false)
|
||||||
|
zora_node.set_physics_process(false)
|
||||||
|
dunja_state.set_hunger(72.0)
|
||||||
|
zora_state.set_hunger(72.0)
|
||||||
|
var dunja_interaction_position := dunja_node.get_interaction_position()
|
||||||
|
var zora_interaction_position := zora_node.get_interaction_position()
|
||||||
|
var tie_position := (dunja_interaction_position + zora_interaction_position) * 0.5
|
||||||
|
var pair_range := dunja_interaction_position.distance_to(zora_interaction_position) + 0.5
|
||||||
|
player.set("interaction_range", pair_range)
|
||||||
|
|
||||||
|
player.global_position = zora_interaction_position
|
||||||
|
var nearest_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
nearest_context != null
|
||||||
|
and nearest_context.kind == PlayerInteractionResult.KIND_ANIMAL
|
||||||
|
and nearest_context.action_id == SimulationIds.ACTION_FEED_ANIMAL
|
||||||
|
and nearest_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||||
|
and nearest_context.display_name == "Zora"
|
||||||
|
and nearest_context.target_node == zora_node
|
||||||
|
and nearest_context.is_available()
|
||||||
|
and "Zora" in nearest_context.prompt_text
|
||||||
|
),
|
||||||
|
"With both hungry goats in range, the context should name exact nearest goat Zora",
|
||||||
|
)
|
||||||
|
hud.call("refresh_prompt")
|
||||||
|
var action_label := hud.get_node("Copy/Action") as Label
|
||||||
|
var detail_label := hud.get_node("Copy/Detail") as Label
|
||||||
|
_check(
|
||||||
|
nearest_context != null and action_label != null and detail_label != null,
|
||||||
|
"The prompt proof needs a resolved context and both public HUD labels",
|
||||||
|
)
|
||||||
|
if nearest_context == null or action_label == null or detail_label == null:
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
nearest_context.prompt_text in action_label.text
|
||||||
|
and detail_label.text == nearest_context.detail_text
|
||||||
|
),
|
||||||
|
"The HUD prompt should render the same exact context that execution will use",
|
||||||
|
)
|
||||||
|
|
||||||
|
player.global_position = tie_position
|
||||||
|
var tie_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
tie_context != null
|
||||||
|
and tie_position.distance_to(dunja_interaction_position) <= pair_range
|
||||||
|
and tie_position.distance_to(zora_interaction_position) <= pair_range
|
||||||
|
and tie_context.target_id == SimulationIds.ANIMAL_DUNJA
|
||||||
|
and tie_context.display_name == "Dunja"
|
||||||
|
and tie_context.target_node == dunja_node
|
||||||
|
),
|
||||||
|
"An equal-distance pair should resolve by stable animal ID, choosing Dunja",
|
||||||
|
)
|
||||||
|
|
||||||
|
player.global_position = zora_interaction_position
|
||||||
|
var execution_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||||
|
var pantry: StorageStateRecord = manager.get_pantry()
|
||||||
|
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||||
|
var dunja_hunger_before := dunja_state.get_hunger()
|
||||||
|
var zora_hunger_before := zora_state.get_hunger()
|
||||||
|
var events_before: int = manager.economic_events.size()
|
||||||
|
player.call("try_interact")
|
||||||
|
var feed_event := _latest_feed_event(manager)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
execution_context != null
|
||||||
|
and execution_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||||
|
and is_equal_approx(dunja_state.get_hunger(), dunja_hunger_before)
|
||||||
|
and is_equal_approx(
|
||||||
|
zora_state.get_hunger(), zora_hunger_before - AnimalStateRecord.HUNGER_RELIEF
|
||||||
|
)
|
||||||
|
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
|
||||||
|
and manager.economic_events.size() == events_before + 1
|
||||||
|
),
|
||||||
|
"Interaction should feed the prompted Zora only and consume one real pantry food",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
feed_event != null
|
||||||
|
and int(feed_event.data["actor_id"]) == -1
|
||||||
|
and StringName(feed_event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||||
|
and StringName(feed_event.data["destination_id"]) == SimulationIds.ANIMAL_ZORA
|
||||||
|
and StringName(feed_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||||
|
and is_equal_approx(float(feed_event.data["amount"]), 1.0)
|
||||||
|
and feed_event.get_world_position().is_equal_approx(zora_state.get_position())
|
||||||
|
),
|
||||||
|
"The player feed event should preserve its exact pantry, named target, amount, and position",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
bool(hud.get("feedback_active")) and bool(hud.get("feedback_succeeded")),
|
||||||
|
"A successful exact interaction should surface transient positive HUD feedback",
|
||||||
|
)
|
||||||
|
|
||||||
|
dunja_state.set_hunger(72.0)
|
||||||
|
zora_state.set_hunger(72.0)
|
||||||
|
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
||||||
|
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||||
|
player.set("interaction_range", 100.0)
|
||||||
|
player.global_position = zora_node.get_interaction_position()
|
||||||
|
var blocked_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||||
|
hud.call("refresh_prompt")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
blocked_context != null
|
||||||
|
and blocked_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||||
|
and blocked_context.display_name == "Zora"
|
||||||
|
and not blocked_context.is_available()
|
||||||
|
and not blocked_context.blocked_reason.is_empty()
|
||||||
|
and "Zora" in (blocked_context.prompt_text + blocked_context.detail_text)
|
||||||
|
and (
|
||||||
|
"pantry"
|
||||||
|
in (blocked_context.blocked_reason + blocked_context.detail_text).to_lower()
|
||||||
|
)
|
||||||
|
and blocked_context.prompt_text in action_label.text
|
||||||
|
and detail_label.text == blocked_context.detail_text
|
||||||
|
),
|
||||||
|
"An empty pantry should show a named blocked Zora prompt before input",
|
||||||
|
)
|
||||||
|
|
||||||
|
var blocked_dunja_hunger := dunja_state.get_hunger()
|
||||||
|
var blocked_zora_hunger := zora_state.get_hunger()
|
||||||
|
var blocked_event_count: int = manager.economic_events.size()
|
||||||
|
var blocked_safety: float = manager.village.safety
|
||||||
|
var blocked_knowledge: float = manager.village.knowledge
|
||||||
|
player.call("try_interact")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
is_equal_approx(dunja_state.get_hunger(), blocked_dunja_hunger)
|
||||||
|
and is_equal_approx(zora_state.get_hunger(), blocked_zora_hunger)
|
||||||
|
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
||||||
|
and manager.economic_events.size() == blocked_event_count
|
||||||
|
and is_equal_approx(manager.village.safety, blocked_safety)
|
||||||
|
and is_equal_approx(manager.village.knowledge, blocked_knowledge)
|
||||||
|
),
|
||||||
|
"A blocked animal interaction should consume input without mutation, events, or fallthrough",
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
bool(hud.get("feedback_active")) and not bool(hud.get("feedback_succeeded")),
|
||||||
|
"The blocked exact interaction should surface transient negative HUD feedback",
|
||||||
|
)
|
||||||
|
|
||||||
|
var blocked_json: String = manager.serialize_state()
|
||||||
|
dunja_state.set_hunger(20.0)
|
||||||
|
zora_state.set_hunger(20.0)
|
||||||
|
pantry.deposit(SimulationIds.RESOURCE_FOOD, 2.0)
|
||||||
|
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||||
|
_check(
|
||||||
|
manager.restore_state_from_json(blocked_json),
|
||||||
|
"The blocked two-goat context should restore through the complete state schema",
|
||||||
|
)
|
||||||
|
dunja_state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||||
|
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||||
|
pantry = manager.get_pantry()
|
||||||
|
var restored_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
not bool(hud.get("feedback_active"))
|
||||||
|
and restored_context != null
|
||||||
|
and restored_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||||
|
and not restored_context.is_available()
|
||||||
|
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
||||||
|
and is_equal_approx(dunja_state.get_hunger(), blocked_dunja_hunger)
|
||||||
|
and is_equal_approx(zora_state.get_hunger(), blocked_zora_hunger)
|
||||||
|
and restored_context.prompt_text in action_label.text
|
||||||
|
and detail_label.text == restored_context.detail_text
|
||||||
|
),
|
||||||
|
"Restore should clear transient feedback and re-derive the named blocked prompt",
|
||||||
|
)
|
||||||
|
|
||||||
|
var demo_controller := main_scene.get_node("DemoController")
|
||||||
|
var debug_ui := main_scene.get_node("UI") as CanvasLayer
|
||||||
|
demo_controller.call("set_debug_overlay_visible", false)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
not debug_ui.visible
|
||||||
|
and bool(hud.get("visible"))
|
||||||
|
and action_label.visible
|
||||||
|
and not action_label.text.is_empty()
|
||||||
|
),
|
||||||
|
"The player interaction HUD should remain visible when development overlays are hidden",
|
||||||
|
)
|
||||||
|
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _latest_feed_event(manager: Node) -> EconomicEventRecord:
|
||||||
|
for index in range(manager.economic_events.size() - 1, -1, -1):
|
||||||
|
var event := manager.economic_events[index] as EconomicEventRecord
|
||||||
|
if StringName(event.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED:
|
||||||
|
return event
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Player context interaction passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://47l78j2djkv0
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
class_name PlayerInteractionHud
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const FEEDBACK_SECONDS := 2.0
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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 not feedback_active:
|
||||||
|
refresh_prompt()
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_prompt(force: bool = false) -> void:
|
||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://caga5h0ri6vei
|
||||||
Reference in New Issue
Block a user