feat: share village needs with helpers
This commit is contained in:
@@ -75,11 +75,13 @@ It also publishes the latest `ActionSelectionResult` for presentation; the UI
|
||||
does not recompute decisions. Each idle selection re-derives the capable helper
|
||||
instead of consulting persisted assignment state. At completion it atomically
|
||||
pays any definition-backed stored-resource cost before applying the action
|
||||
effect. A late shortfall suppresses the effect and records a `task_blocked` fact. The
|
||||
manager also captures actor/nearby knowledge before forwarding newly recorded
|
||||
events into `RelationshipSystem`. When an NPC arrives beside a worker at the
|
||||
same non-storage activity site, the manager may transfer one direct known fact
|
||||
and applies its consequence only to that newly informed listener. It exposes
|
||||
effect. A late shortfall suppresses the effect and records a `task_blocked`
|
||||
fact. The manager also captures actor/nearby knowledge before forwarding newly
|
||||
recorded events into `RelationshipSystem`. When an NPC arrives beside a worker
|
||||
at the same non-storage activity site, the manager may transfer one direct
|
||||
known fact and applies its consequence only to that newly informed listener. An
|
||||
open opportunity's exact trigger receives bounded priority in that conversation
|
||||
before normal lasting/recent ranking. It exposes
|
||||
knowledge, provenance, relationship, and cause queries without moving social
|
||||
authority into UI. After consequences are known, it protects current causal
|
||||
facts, enforces the recent-memory cap, and runs age review from authoritative
|
||||
@@ -108,8 +110,9 @@ simulation ticks.
|
||||
legacy, with a stable source NPC ID for direct communication;
|
||||
- permits one performed/witnessed fact to cross one co-located social hop, but
|
||||
does not relay communicated or ambiguous legacy facts;
|
||||
- orders communication by lasting relationship relevance, then acquisition
|
||||
recency;
|
||||
- tries the active opportunity trigger first when the speaker directly knows
|
||||
it, then orders remaining communication by lasting relationship relevance
|
||||
and acquisition recency;
|
||||
- retains current relationship causes and at most three other recent facts per
|
||||
NPC, expiring day-old recent facts at deterministic review boundaries;
|
||||
- captures evidence only when the event happens, never from current positions
|
||||
|
||||
@@ -38,7 +38,8 @@ would otherwise obscure that lifecycle:
|
||||
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
|
||||
known objective events, immutable acquisition provenance, proximity witnesses
|
||||
at record time, one-hop direct fact transfer, and deterministic recent-fact
|
||||
retention/importance ranking;
|
||||
retention/importance ranking. It can place the active opportunity trigger
|
||||
first without weakening its direct-source or one-hop checks;
|
||||
- `simulation/relationships/RelationshipSystem.gd` owns directed relationship
|
||||
queries, event-driven trust changes, and deterministic social tie-breaking;
|
||||
- `simulation/opportunities/VillageOpportunitySystem.gd` observes immutable
|
||||
@@ -96,8 +97,10 @@ improving ownership.
|
||||
retained knowledge. Opportunity presentation is likewise query-only.
|
||||
Relationship cues, interested-villager concern, physical pantry stock, and
|
||||
refill feedback consume authoritative state or state-change signals and are
|
||||
intentionally absent from saves and checksums. Stable visuals are rebuilt
|
||||
from state after restore; transient reactions are not replayed.
|
||||
intentionally absent from saves and checksums. The transient village-whisper
|
||||
HUD likewise formats need, transfer, response, and resolution signals without
|
||||
owning them. Stable visuals are rebuilt from state after restore; transient
|
||||
reactions are not replayed.
|
||||
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
||||
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
||||
- New mutable features define serialization and deterministic continuation at
|
||||
|
||||
@@ -741,13 +741,18 @@ Completed:
|
||||
and sleep/meal precedence. Existing target resolution chooses and reserves
|
||||
the finite source, while no quest acceptance, reward, or helper assignment
|
||||
is introduced.
|
||||
30. Village whisper: direct witnesses/performers now report the active need's
|
||||
exact trigger before unrelated facts at shared work, allowing a newly
|
||||
informed trusted helper to respond autonomously. One cardless warm-amber HUD
|
||||
ribbon gently rises, pulses, and fades through need, named report, response,
|
||||
and resolution beats while remaining separate from development overlays.
|
||||
|
||||
Next:
|
||||
|
||||
1. Prove one direct information-to-help chain: transfer the exact open-need
|
||||
trigger at an existing shared activity, then let the newly informed trusted
|
||||
villager be re-derived and respond through the same autonomous selector.
|
||||
Keep generic dialogue and multi-hop rumours out of scope, as sequenced in
|
||||
1. When no capable helper exists, show one contextual player response hint
|
||||
derived from a current player-usable finite source and the real storage
|
||||
target. Clear it as soon as help emerges or the need closes; do not add quest
|
||||
acceptance, waypoint state, or a generic tracker, as sequenced in
|
||||
`LEARNING_ROADMAP.md`.
|
||||
|
||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||
|
||||
@@ -99,6 +99,14 @@ opening; invalidation emits no event and stores the close tick and stable
|
||||
reason. Resolution or invalidation returns the trigger to normal bounded
|
||||
retention.
|
||||
|
||||
When a performer or direct witness meets another villager at an eligible shared
|
||||
activity, the active opportunity's trigger is attempted before normal
|
||||
lasting/recent communication ranking. `EventKnowledgeSystem` still requires
|
||||
performed/witnessed source provenance and records only the existing event ID,
|
||||
speaker ID, source method, listener, and acquisition tick. A newer unrelated
|
||||
fact cannot displace the active report, while communicated listeners still
|
||||
cannot relay it onward.
|
||||
|
||||
For an open need, the same system can derive one capable helper without
|
||||
assigning work. A candidate must know the exact trigger and have a familiar
|
||||
directed relationship with at least 0.6 trust toward the interested villager.
|
||||
@@ -148,6 +156,10 @@ world cue remains specific to the pantry shortage rather than representing a
|
||||
wood need as hunger. The active summary additionally names the currently
|
||||
derived helper, supply action, trust fact, and inventory or finite-source route,
|
||||
or states that no informed trusted villager is currently able to supply it.
|
||||
One separate cardless player HUD gives transient warm-amber feedback for the
|
||||
authoritative need opening, exact named report, newly informed helper decision,
|
||||
and real resolution. It stays available when development overlays are hidden,
|
||||
but clears on restore and never becomes objective or save state.
|
||||
|
||||
## Deliberate limits
|
||||
|
||||
|
||||
@@ -807,13 +807,27 @@ The first bounded autonomous helper consumer is complete:
|
||||
- focused headless and Jajce runtime regressions prove autonomous selection,
|
||||
precedence, finite-source targeting, reservation, and restore-time re-query.
|
||||
|
||||
Milestone 7 is not complete. The immediate next slice should connect the
|
||||
existing one-hop fact transfer to this path: prove that a direct witness or
|
||||
performer can communicate an open need's exact trigger at a real shared
|
||||
activity, after which the newly informed trusted villager is re-derived as the
|
||||
helper and responds through the same autonomous selection contract. Keep this
|
||||
as a truthful event-reference transfer, not generic dialogue or multi-hop
|
||||
rumour infrastructure.
|
||||
The first direct information-to-help path is complete:
|
||||
|
||||
- at an eligible shared activity, a speaker who performed or witnessed the
|
||||
active need's exact trigger discusses it before other retained direct facts;
|
||||
- the transfer still references the original event, preserves the speaker and
|
||||
direct source method, stops after one hop, and creates no dialogue/event copy;
|
||||
- a regression places a newer unrelated fact ahead under normal ranking, then
|
||||
proves active-need priority transfers only the trigger to the trusted capable
|
||||
listener;
|
||||
- the newly informed villager is immediately re-derived as helper and, once
|
||||
idle, chooses the same ordinary autonomous gather/deposit path;
|
||||
- a cardless warm-amber player HUD briefly shows the real need, named report,
|
||||
helper response, and resolution. It is signal-driven, survives debug-overlay
|
||||
hiding, and neither serializes nor replays after restore.
|
||||
|
||||
Milestone 7 is not complete. The immediate next slice should expose one bounded
|
||||
player response affordance when an open need has no capable helper: derive a
|
||||
concise contextual hint from the existing player-usable finite source and real
|
||||
storage target, then clear it when a helper emerges or the need closes. Reuse
|
||||
the current interaction/economy rules; do not add quest acceptance, waypoint
|
||||
state, or a generic objective tracker.
|
||||
|
||||
Recently completed:
|
||||
|
||||
|
||||
+20
-7
@@ -472,7 +472,9 @@ follow camera.
|
||||
The UI subscribes to village, task, event, knowledge, relationship, and
|
||||
opportunity changes. It formats aggregate state and resolves known facts,
|
||||
relationship causes, and opportunity lifecycle details through manager queries
|
||||
without owning or recomputing simulation facts.
|
||||
without owning or recomputing simulation facts. `village_whisper_hud.gd` is a
|
||||
separate transient player layer that formats the same authoritative signals and
|
||||
remains independent of the development overlays.
|
||||
|
||||
## Current runtime flow
|
||||
|
||||
@@ -556,6 +558,7 @@ NpcVisual navigates through the active world
|
||||
│ ├── knowledge_retention_consequence_test.gd
|
||||
│ ├── jajce_runtime_integration_test.gd
|
||||
│ ├── npc_visual_lifecycle_test.gd
|
||||
│ ├── opportunity_communication_helper_test.gd
|
||||
│ ├── relationship_consequence_test.gd
|
||||
│ ├── resource_node_player_parity_test.gd
|
||||
│ ├── simulation_definitions_test.gd
|
||||
@@ -579,7 +582,9 @@ NpcVisual navigates through the active world
|
||||
│ │ └── ResourceNode.tscn
|
||||
│ ├── active_world_adapter.gd
|
||||
│ ├── world_view_manager.gd
|
||||
│ └── ui/ui.gd
|
||||
│ └── ui/
|
||||
│ ├── ui.gd
|
||||
│ └── village_whisper_hud.gd
|
||||
├── main.tscn
|
||||
└── project.godot
|
||||
```
|
||||
@@ -922,11 +927,19 @@ The ordinary in-progress NPC task can save normally, but no helper assignment,
|
||||
quest acceptance, reward, or opportunity-specific event is persisted; ordinary
|
||||
task-start history remains unchanged.
|
||||
|
||||
Milestone 7 remains in progress. The immediate next slice should prove the
|
||||
combined information-to-help path: communicate an open need's exact trigger
|
||||
once at a real shared activity, then re-derive the newly informed trusted NPC as
|
||||
the autonomous helper. Keep the transfer direct and source-backed rather than
|
||||
introducing generic dialogue or multi-hop rumours.
|
||||
The first direct information-to-help path is complete. At shared activity, a
|
||||
performer or direct witness now discusses the active need's exact trigger before
|
||||
newer unrelated facts. The original event and first-source provenance remain
|
||||
authoritative and one-hop. A newly informed trusted, supplied villager is then
|
||||
re-derived and autonomously responds through ordinary action selection. A
|
||||
single cardless warm-amber HUD ribbon presents the real need, named report,
|
||||
helper response, and resolution without entering simulation or save state.
|
||||
|
||||
Milestone 7 remains in progress. The immediate next slice should add one
|
||||
bounded player response hint only when no capable helper exists, derived from
|
||||
the current player-usable finite source and real destination. It should clear
|
||||
when help emerges or the need closes, without quest acceptance, waypoint state,
|
||||
or a generic tracker.
|
||||
|
||||
The remaining simulation-garden target still aims for:
|
||||
|
||||
|
||||
@@ -207,6 +207,11 @@ existing NPC task, task-state, target, travel, and reservation fields serialize.
|
||||
The helper result remains absent: restore retains any in-progress ordinary task
|
||||
and independently re-derives current capability from restored facts.
|
||||
|
||||
Active-need communication adds no schema fields. It persists through the
|
||||
existing `KnownEventStateRecord` event, listener, source NPC, direct source
|
||||
method, and acquisition-tick fields. Communication/HUD animations are transient
|
||||
and are cleared rather than replayed after restore.
|
||||
|
||||
## Resource authority
|
||||
|
||||
`SimulationManager` owns `ResourceStateRecord` instances independently of the
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[ext_resource type="Script" uid="uid://cascjhf8lsvay" path="res://world/ui/time_dial.gd" id="12_timedial"]
|
||||
[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="Script" uid="uid://dwwxux24jc6yq" path="res://world/ui/village_whisper_hud.gd" id="15_whisper"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
||||
radius = 0.4
|
||||
@@ -126,6 +127,66 @@ offset_right = 940.0
|
||||
offset_bottom = 66.0
|
||||
script = ExtResource("12_timedial")
|
||||
|
||||
[node name="VillageWhisperLayer" type="CanvasLayer" parent="."]
|
||||
layer = 3
|
||||
|
||||
[node name="VillageWhisper" type="Control" parent="VillageWhisperLayer" node_paths=PackedStringArray("simulation_manager")]
|
||||
anchors_preset = 10
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -310.0
|
||||
offset_top = 76.0
|
||||
offset_right = 310.0
|
||||
offset_bottom = 142.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("15_whisper")
|
||||
simulation_manager = NodePath("../../SimulationManager")
|
||||
|
||||
[node name="Accent" type="ColorRect" parent="VillageWhisperLayer/VillageWhisper"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 2.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0.86, 0.61, 0.25, 0.92)
|
||||
|
||||
[node name="Copy" type="VBoxContainer" parent="VillageWhisperLayer/VillageWhisper"]
|
||||
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 = 1
|
||||
alignment = 1
|
||||
|
||||
[node name="Kicker" type="Label" parent="VillageWhisperLayer/VillageWhisper/Copy"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_colors/font_color = Color(0.93, 0.7, 0.36, 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 = 11
|
||||
text = "VILLAGE NEED"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Message" type="Label" parent="VillageWhisperLayer/VillageWhisper/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 = 18
|
||||
text = "A village need has surfaced."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CrisisCaptionLayer" type="CanvasLayer" parent="."]
|
||||
layer = 4
|
||||
|
||||
|
||||
@@ -127,11 +127,9 @@ func generate_npcs() -> void:
|
||||
npc_random.randf_range(1.0, 10.0),
|
||||
npc_random
|
||||
)
|
||||
|
||||
npc.debug_logs = debug_logs
|
||||
npcs.append(npc)
|
||||
wander_random_sources[i] = _create_random_source(i, 1)
|
||||
|
||||
var home_count := home_positions.size()
|
||||
if home_count > 0:
|
||||
for i in range(npcs.size()):
|
||||
@@ -632,7 +630,9 @@ func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bo
|
||||
if not _can_communicate_at_shared_activity(speaker, listener):
|
||||
return false
|
||||
var known_event_ids := event_knowledge_system.get_communicable_event_ids(
|
||||
speaker.id, _get_lasting_event_ids(speaker.id)
|
||||
speaker.id,
|
||||
_get_lasting_event_ids(speaker.id),
|
||||
opportunity_system.get_open_trigger_event_id()
|
||||
)
|
||||
for event_id in known_event_ids:
|
||||
var event := event_log.get_by_id(event_id)
|
||||
|
||||
@@ -109,8 +109,14 @@ func get_retained_event_ids(
|
||||
return _get_ranked_event_ids(knower_id, lasting_event_ids, false, max_count)
|
||||
|
||||
|
||||
func get_communicable_event_ids(knower_id: int, lasting_event_ids: Array[int]) -> Array[int]:
|
||||
return _get_ranked_event_ids(knower_id, lasting_event_ids, true, 0)
|
||||
func get_communicable_event_ids(
|
||||
knower_id: int, lasting_event_ids: Array[int], preferred_event_id: int = -1
|
||||
) -> Array[int]:
|
||||
var event_ids := _get_ranked_event_ids(knower_id, lasting_event_ids, true, 0)
|
||||
if preferred_event_id in event_ids:
|
||||
event_ids.erase(preferred_event_id)
|
||||
event_ids.push_front(preferred_event_id)
|
||||
return event_ids
|
||||
|
||||
|
||||
func _get_ranked_event_ids(
|
||||
|
||||
@@ -99,6 +99,15 @@ func get_open_opportunity() -> OpportunityStateRecord:
|
||||
return open_records[0] if not open_records.is_empty() else null
|
||||
|
||||
|
||||
func get_open_trigger_event_id() -> int:
|
||||
var opportunity := get_open_opportunity()
|
||||
return (
|
||||
opportunity.get_trigger_event_id()
|
||||
if opportunity != null
|
||||
else OpportunityStateRecord.NO_EVENT_ID
|
||||
)
|
||||
|
||||
|
||||
func get_latest() -> OpportunityStateRecord:
|
||||
var sorted := get_all_sorted()
|
||||
return sorted[-1] if not sorted.is_empty() else null
|
||||
|
||||
@@ -77,6 +77,15 @@ func _run() -> void:
|
||||
"Restarting the transient trust blossom must not mutate simulation state"
|
||||
)
|
||||
var village_ui := main_scene.get_node("UI")
|
||||
var village_whisper := (
|
||||
main_scene.get_node("VillageWhisperLayer/VillageWhisper") as VillageWhisperHud
|
||||
)
|
||||
var whisper_kicker := village_whisper.get_node("Copy/Kicker") as Label
|
||||
var whisper_message := village_whisper.get_node("Copy/Message") as Label
|
||||
_check(
|
||||
not village_whisper.visible,
|
||||
"The player-facing village whisper should begin quiet instead of replaying history"
|
||||
)
|
||||
village_ui.selected_npc_index = witness.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
_check(
|
||||
@@ -250,8 +259,9 @@ func _run() -> void:
|
||||
and pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.EMPTY
|
||||
and pantry_visual.get_node("EmptyStock").visible
|
||||
and not pantry_visual.get_node("RefillResponse").visible
|
||||
and not village_whisper.visible
|
||||
),
|
||||
"Restoration should rebuild concern and empty stock without replaying the refill response"
|
||||
"Restore should rebuild stable cues without replaying refill or player-HUD feedback"
|
||||
)
|
||||
var player := main_scene.get_node("Player") as Node3D
|
||||
var opportunity_bush := (
|
||||
@@ -270,8 +280,10 @@ func _run() -> void:
|
||||
player_opportunity.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
||||
and "Resolved village need" in inspector_label.text
|
||||
and "◆ Player restocked the pantry" in inspector_label.text
|
||||
and whisper_kicker.text == "NEED MET"
|
||||
and "The player restocked the pantry" in whisper_message.text
|
||||
),
|
||||
"Player gathering should resolve the same need and be named from its real event"
|
||||
"Player gathering should resolve the restored need in debug detail and transient HUD"
|
||||
)
|
||||
|
||||
var woodpile_state: StorageStateRecord = simulation_manager.get_woodpile()
|
||||
@@ -283,7 +295,9 @@ func _run() -> void:
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile") as StorageNode
|
||||
)
|
||||
contributor.position = woodpile_node.get_interaction_position()
|
||||
witness.position = contributor.position
|
||||
for npc in simulation_manager.npcs:
|
||||
if npc.id != contributor.id:
|
||||
npc.position = contributor.position + Vector3(20.0 + npc.id * 2.0, 0.0, 0.0)
|
||||
witness.profession = SimulationIds.PROFESSION_WOODCUTTER
|
||||
contributor.set_task(SimulationIds.ACTION_STUDY, 1.0)
|
||||
contributor.target_id = SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||
@@ -292,14 +306,64 @@ func _run() -> void:
|
||||
simulation_manager.simulate_tick()
|
||||
village_ui.selected_npc_index = contributor.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
var runtime_opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
var runtime_trigger_id := (
|
||||
runtime_opportunity.get_trigger_event_id() if runtime_opportunity != null else -1
|
||||
)
|
||||
var runtime_helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
||||
_check(
|
||||
(
|
||||
runtime_helper != null
|
||||
runtime_opportunity != null
|
||||
and runtime_helper == null
|
||||
and not simulation_manager.npc_knows_event(witness.id, runtime_trigger_id)
|
||||
and whisper_kicker.text == "VILLAGE NEED"
|
||||
and contributor.npc_name in whisper_message.text
|
||||
and "needs wood" in whisper_message.text
|
||||
),
|
||||
"The real blocked-work need should surface while the distant helper remains uninformed"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
"◆ Supply wood for blocked work" in village_stats_label.text
|
||||
and "could not finish Study" in village_stats_label.text
|
||||
and (
|
||||
"Possible helper: none informed, trusted, and able to supply"
|
||||
in village_stats_label.text
|
||||
)
|
||||
and "◆ Find wood for Study" in inspector_label.text
|
||||
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
||||
),
|
||||
"The missing-wood UI should honestly show that no capable informed helper exists yet"
|
||||
)
|
||||
|
||||
contributor.set_task(SimulationIds.ACTION_PATROL)
|
||||
contributor.target_id = guard_site.site_id
|
||||
contributor.position = guard_site.get_interaction_position()
|
||||
contributor.start_working()
|
||||
witness.set_task(SimulationIds.ACTION_PATROL)
|
||||
witness.target_id = guard_site.site_id
|
||||
witness.position = contributor.position + Vector3(1.0, 0.0, 0.0)
|
||||
simulation_manager.notify_npc_arrived(witness.id)
|
||||
var communicated_trigger: KnownEventStateRecord = simulation_manager.get_known_event_record(
|
||||
witness.id, runtime_trigger_id
|
||||
)
|
||||
runtime_helper = simulation_manager.get_active_opportunity_helper()
|
||||
_check(
|
||||
(
|
||||
communicated_trigger != null
|
||||
and (
|
||||
communicated_trigger.get_acquisition_method()
|
||||
== SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||
)
|
||||
and communicated_trigger.get_source_npc_id() == contributor.id
|
||||
and runtime_helper != null
|
||||
and runtime_helper.helper_npc_id == witness.id
|
||||
and runtime_helper.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||
and whisper_kicker.text == "NEWS TRAVELS"
|
||||
and contributor.npc_name in whisper_message.text
|
||||
and witness.npc_name in whisper_message.text
|
||||
),
|
||||
"Runtime helper query should derive the informed trusted woodcutter"
|
||||
"A real shared activity should report the exact need and reveal the trusted helper"
|
||||
)
|
||||
var runtime_helper_reason := (
|
||||
"Knows the need; trust %.2f toward %s" % [runtime_helper.trust, contributor.npc_name]
|
||||
@@ -315,7 +379,7 @@ func _run() -> void:
|
||||
and "◆ Find wood for Study" in inspector_label.text
|
||||
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
||||
),
|
||||
"A real missing-wood fact should surface through wood-specific UI without a food cue"
|
||||
"The compact summary should update from the newly communicated simulation fact"
|
||||
)
|
||||
witness.current_task = SimulationIds.ACTION_IDLE
|
||||
witness.task_state = SimNPC.TASK_STATE_IDLE
|
||||
@@ -338,9 +402,19 @@ func _run() -> void:
|
||||
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
|
||||
and helper_resource != null
|
||||
and helper_resource.get_reserved_by() == witness.id
|
||||
and whisper_kicker.text == "HELP IS ON THE WAY"
|
||||
and witness.npc_name in whisper_message.text
|
||||
and "gather wood" in whisper_message.text
|
||||
),
|
||||
"The runtime helper should autonomously enter normal target resolution and reservation"
|
||||
"The informed helper and cozy HUD should reflect ordinary target resolution and reservation"
|
||||
)
|
||||
var demo_controller := main_scene.get_node("DemoController")
|
||||
demo_controller.set_debug_overlay_visible(false)
|
||||
_check(
|
||||
not village_ui.visible and village_whisper.visible,
|
||||
"The transient player HUD should remain legible when development overlays are hidden"
|
||||
)
|
||||
demo_controller.set_debug_overlay_visible(true)
|
||||
contributor.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0)
|
||||
_check(
|
||||
is_equal_approx(
|
||||
@@ -354,8 +428,10 @@ func _run() -> void:
|
||||
(
|
||||
"Village need" not in village_stats_label.text
|
||||
and "◆ %s supplied the woodpile" % contributor.npc_name in inspector_label.text
|
||||
and whisper_kicker.text == "NEED MET"
|
||||
and "supplied the woodpile" in whisper_message.text
|
||||
),
|
||||
"The resolved wood need should name its exact supplier and leave the active summary"
|
||||
"Resolution should name its supplier in both debug detail and the transient player HUD"
|
||||
)
|
||||
_check(
|
||||
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
var transfers: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var manager := _create_manager(1301)
|
||||
manager.event_knowledge_transferred.connect(_on_event_knowledge_transferred)
|
||||
var interested: SimNPC = manager.npcs[0]
|
||||
var speaker: SimNPC = manager.npcs[1]
|
||||
var helper: SimNPC = manager.npcs[2]
|
||||
interested.position = Vector3.ZERO
|
||||
speaker.position = Vector3(4.0, 0.0, 0.0)
|
||||
helper.position = Vector3(30.0, 0.0, 0.0)
|
||||
for npc in manager.npcs:
|
||||
npc.hunger = 20.0
|
||||
npc.energy = 80.0
|
||||
_set_wood_amount(manager, 0.5)
|
||||
|
||||
interested.set_task(SimulationIds.ACTION_STUDY, 1.0)
|
||||
interested.start_working()
|
||||
manager.simulate_tick()
|
||||
var opportunity: OpportunityStateRecord = manager.get_active_opportunity()
|
||||
var trigger := _latest_event_of_type(manager, SimulationIds.EVENT_TASK_BLOCKED, interested.id)
|
||||
_check(opportunity != null and trigger != null, "Blocked work should open a real wood need")
|
||||
if opportunity == null or trigger == null:
|
||||
manager.free()
|
||||
_finish()
|
||||
return
|
||||
var trigger_id := int(trigger.data["event_id"])
|
||||
_check(
|
||||
(
|
||||
manager.npc_knows_event(interested.id, trigger_id)
|
||||
and manager.npc_knows_event(speaker.id, trigger_id)
|
||||
and not manager.npc_knows_event(helper.id, trigger_id)
|
||||
and (
|
||||
manager.get_known_event_record(speaker.id, trigger_id).get_acquisition_method()
|
||||
== SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
),
|
||||
"The future speaker should directly witness the need while the helper remains uninformed"
|
||||
)
|
||||
|
||||
helper.profession = SimulationIds.PROFESSION_WOODCUTTER
|
||||
_add_trusted_relationship(manager, helper.id, interested.id)
|
||||
var tree := _create_helper_tree()
|
||||
root.add_child(tree)
|
||||
await process_frame
|
||||
_check(manager.register_resource_node(tree), "The helper route should use a finite tree")
|
||||
_check(
|
||||
manager.get_active_opportunity_helper() == null,
|
||||
"Trust and supply should not make an uninformed villager a helper"
|
||||
)
|
||||
|
||||
interested.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
_check(
|
||||
manager.economy.deposit_inventory(interested, SimulationIds.RESOURCE_FOOD) > 0.0,
|
||||
"Setup should create one newer directly witnessed distractor fact"
|
||||
)
|
||||
var distractor := _latest_event_of_type(
|
||||
manager, SimulationIds.EVENT_STORAGE_DEPOSITED, interested.id
|
||||
)
|
||||
_check(distractor != null, "The distractor deposit should enter objective history")
|
||||
if distractor == null:
|
||||
tree.queue_free()
|
||||
manager.free()
|
||||
_finish()
|
||||
return
|
||||
var distractor_id := int(distractor.data["event_id"])
|
||||
var no_lasting_events: Array[int] = []
|
||||
var default_order: Array[int] = manager.event_knowledge_system.get_communicable_event_ids(
|
||||
speaker.id, no_lasting_events
|
||||
)
|
||||
_check(
|
||||
not default_order.is_empty() and default_order[0] == distractor_id,
|
||||
"Without active-need priority, the newer direct fact would be discussed first"
|
||||
)
|
||||
|
||||
_prepare_shared_activity(speaker, helper)
|
||||
var event_count_before: int = manager.economic_events.size()
|
||||
var next_event_id_before: int = manager.next_event_id
|
||||
manager.notify_npc_arrived(helper.id)
|
||||
var communicated: KnownEventStateRecord = manager.get_known_event_record(helper.id, trigger_id)
|
||||
_check(
|
||||
(
|
||||
communicated != null
|
||||
and (
|
||||
communicated.get_acquisition_method()
|
||||
== SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||
)
|
||||
and communicated.get_source_npc_id() == speaker.id
|
||||
and (
|
||||
communicated.get_source_acquisition_method()
|
||||
== SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
and not manager.npc_knows_event(helper.id, distractor_id)
|
||||
and (
|
||||
transfers
|
||||
== [{"speaker_id": speaker.id, "listener_id": helper.id, "event_id": trigger_id}]
|
||||
)
|
||||
),
|
||||
"Shared work should communicate the exact active trigger before newer unrelated facts"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
manager.economic_events.size() == event_count_before
|
||||
and manager.next_event_id == next_event_id_before
|
||||
),
|
||||
"Reporting the need should reference existing history without adding an event"
|
||||
)
|
||||
|
||||
var helper_result: OpportunityHelperResult = manager.get_active_opportunity_helper()
|
||||
_check(
|
||||
(
|
||||
helper_result != null
|
||||
and helper_result.helper_npc_id == helper.id
|
||||
and helper_result.trigger_event_id == trigger_id
|
||||
and helper_result.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||
),
|
||||
"The newly informed trusted woodcutter should be re-derived as the capable helper"
|
||||
)
|
||||
helper.current_task = SimulationIds.ACTION_IDLE
|
||||
helper.task_state = SimNPC.TASK_STATE_IDLE
|
||||
helper.task_complete = true
|
||||
helper.target_id = &""
|
||||
helper.has_travel_target = false
|
||||
manager.simulate_tick()
|
||||
var decision: ActionSelectionResult = manager.get_latest_decision(helper.id)
|
||||
_check(
|
||||
(
|
||||
decision != null
|
||||
and decision.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||
and "Responding to village need" in decision.reason
|
||||
and helper.current_task == SimulationIds.ACTION_GATHER_WOOD
|
||||
),
|
||||
"The communicated fact should flow into ordinary autonomous helper selection"
|
||||
)
|
||||
|
||||
var saved_json: String = manager.serialize_state()
|
||||
var restored := _create_manager(1302)
|
||||
_check(
|
||||
restored.restore_state_from_json(saved_json), "The communicated helper path should restore"
|
||||
)
|
||||
var restored_record: KnownEventStateRecord = restored.get_known_event_record(
|
||||
helper.id, trigger_id
|
||||
)
|
||||
var restored_helper: OpportunityHelperResult = restored.get_active_opportunity_helper()
|
||||
_check(
|
||||
(
|
||||
restored.get_state_checksum() == manager.get_state_checksum()
|
||||
and restored_record != null
|
||||
and restored_record.get_source_npc_id() == speaker.id
|
||||
and restored_helper != null
|
||||
and restored_helper.helper_npc_id == helper.id
|
||||
),
|
||||
"Restore should preserve provenance and re-derive capability without assignment state"
|
||||
)
|
||||
|
||||
restored.free()
|
||||
manager.free()
|
||||
tree.queue_free()
|
||||
await process_frame
|
||||
_finish()
|
||||
|
||||
|
||||
func _prepare_shared_activity(speaker: SimNPC, listener: SimNPC) -> void:
|
||||
speaker.set_task(SimulationIds.ACTION_PATROL)
|
||||
speaker.target_id = &"opportunity_guard_post"
|
||||
speaker.position = Vector3(20.0, 0.0, 0.0)
|
||||
speaker.start_working()
|
||||
listener.set_task(SimulationIds.ACTION_PATROL)
|
||||
listener.target_id = speaker.target_id
|
||||
listener.position = speaker.position + Vector3(1.0, 0.0, 0.0)
|
||||
|
||||
|
||||
func _add_trusted_relationship(manager: Node, observer_id: int, subject_id: int) -> void:
|
||||
var records: Array[RelationshipStateRecord] = manager.relationship_system.get_all_sorted()
|
||||
for index in records.size():
|
||||
var relationship := records[index]
|
||||
if (
|
||||
relationship.get_observer_id() == observer_id
|
||||
and relationship.get_subject_id() == subject_id
|
||||
):
|
||||
records[index] = RelationshipStateRecord.create(observer_id, subject_id, 0.7, 0.7)
|
||||
manager.relationship_system.restore(records)
|
||||
return
|
||||
records.append(RelationshipStateRecord.create(observer_id, subject_id, 0.7, 0.7))
|
||||
manager.relationship_system.restore(records)
|
||||
|
||||
|
||||
func _set_wood_amount(manager: Node, amount: float) -> void:
|
||||
var woodpile: StorageStateRecord = manager.get_woodpile()
|
||||
woodpile.withdraw(SimulationIds.RESOURCE_WOOD, woodpile.get_amount(SimulationIds.RESOURCE_WOOD))
|
||||
woodpile.deposit(SimulationIds.RESOURCE_WOOD, amount)
|
||||
manager.economy.sync_resource(SimulationIds.RESOURCE_WOOD)
|
||||
|
||||
|
||||
func _latest_event_of_type(
|
||||
manager: Node, event_type: StringName, actor_id: int
|
||||
) -> EconomicEventRecord:
|
||||
var events: Array = manager.get_npc_events(actor_id, 8)
|
||||
for index in range(events.size() - 1, -1, -1):
|
||||
var event := events[index] as EconomicEventRecord
|
||||
if StringName(event.data["event_type"]) == event_type:
|
||||
return event
|
||||
return null
|
||||
|
||||
|
||||
func _create_helper_tree() -> ResourceNode:
|
||||
var tree := ResourceNode.new()
|
||||
tree.name = "CommunicatedHelperTree"
|
||||
tree.node_id = &"communicated_helper_tree"
|
||||
tree.action_id = SimulationIds.ACTION_GATHER_WOOD
|
||||
tree.resource_id = SimulationIds.RESOURCE_WOOD
|
||||
tree.initial_amount = 2.0
|
||||
tree.yield_per_action = 1.0
|
||||
tree.can_npcs_use = true
|
||||
tree.can_player_use = false
|
||||
tree.debug_label_enabled = false
|
||||
var interaction_point := Marker3D.new()
|
||||
interaction_point.name = "InteractionPoint"
|
||||
tree.add_child(interaction_point)
|
||||
return tree
|
||||
|
||||
|
||||
func _create_manager(seed_value: int) -> Node:
|
||||
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||
manager.simulation_seed = seed_value
|
||||
manager.debug_logs = false
|
||||
var home_positions: Array[Vector3] = [
|
||||
Vector3(0.0, 0.0, 0.0),
|
||||
Vector3(2.0, 0.0, 0.0),
|
||||
Vector3(20.0, 0.0, 0.0),
|
||||
Vector3(30.0, 0.0, 0.0),
|
||||
Vector3(40.0, 0.0, 0.0),
|
||||
Vector3(50.0, 0.0, 0.0),
|
||||
]
|
||||
manager.home_positions = home_positions
|
||||
root.add_child(manager)
|
||||
manager.set_process(false)
|
||||
for npc in manager.npcs:
|
||||
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
|
||||
npc.start_working()
|
||||
return manager
|
||||
|
||||
|
||||
func _on_event_knowledge_transferred(
|
||||
speaker_id: int, listener_id: int, event: EconomicEventRecord
|
||||
) -> void:
|
||||
transfers.append(
|
||||
{
|
||||
"speaker_id": speaker_id,
|
||||
"listener_id": listener_id,
|
||||
"event_id": int(event.data["event_id"])
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Opportunity communication helper passed: report -> response")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3aeauhllrh1m
|
||||
@@ -137,14 +137,17 @@ func _on_speed_changed(_multiplier: float) -> void:
|
||||
func _on_relationship_changed(
|
||||
_relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord
|
||||
) -> void:
|
||||
_refresh_village_display()
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _on_event_knowledge_changed(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||
_refresh_village_display()
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _on_event_knowledge_forgotten(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||
_refresh_village_display()
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
class_name VillageWhisperHud
|
||||
extends Control
|
||||
|
||||
const DISPLAY_SECONDS := 4.2
|
||||
const ENTER_OFFSET := 8.0
|
||||
|
||||
@export var simulation_manager: Node
|
||||
|
||||
@onready var accent: ColorRect = $Accent
|
||||
@onready var kicker: Label = $Copy/Kicker
|
||||
@onready var message: Label = $Copy/Message
|
||||
|
||||
var feedback_tween: Tween
|
||||
var resting_position: Vector2
|
||||
var feedback_version := 0
|
||||
var active_trigger_event_id := OpportunityStateRecord.NO_EVENT_ID
|
||||
var informed_listener_id := -1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
resting_position = position
|
||||
_reset_feedback()
|
||||
if simulation_manager == null:
|
||||
push_error("VillageWhisperHud: simulation_manager missing")
|
||||
return
|
||||
simulation_manager.opportunity_opened.connect(_on_opportunity_opened)
|
||||
simulation_manager.opportunity_resolved.connect(_on_opportunity_resolved)
|
||||
simulation_manager.opportunity_invalidated.connect(_on_opportunity_invalidated)
|
||||
simulation_manager.event_knowledge_transferred.connect(_on_event_knowledge_transferred)
|
||||
simulation_manager.npc_decision_recorded.connect(_on_npc_decision_recorded)
|
||||
simulation_manager.state_restored.connect(_on_state_restored)
|
||||
|
||||
|
||||
func _on_opportunity_opened(opportunity: OpportunityStateRecord) -> void:
|
||||
active_trigger_event_id = opportunity.get_trigger_event_id()
|
||||
informed_listener_id = -1
|
||||
_show_feedback("Village need", _build_need_message(opportunity))
|
||||
|
||||
|
||||
func _on_event_knowledge_transferred(
|
||||
speaker_id: int, listener_id: int, event: EconomicEventRecord
|
||||
) -> void:
|
||||
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
if opportunity == null or int(event.data["event_id"]) != opportunity.get_trigger_event_id():
|
||||
return
|
||||
active_trigger_event_id = opportunity.get_trigger_event_id()
|
||||
informed_listener_id = listener_id
|
||||
_show_feedback(
|
||||
"News travels",
|
||||
(
|
||||
"%s shares the %s with %s."
|
||||
% [
|
||||
_get_npc_name(speaker_id),
|
||||
_get_need_name(opportunity),
|
||||
_get_npc_name(listener_id),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _on_npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult) -> void:
|
||||
if npc.id != informed_listener_id:
|
||||
return
|
||||
var helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
||||
if (
|
||||
helper == null
|
||||
or helper.trigger_event_id != active_trigger_event_id
|
||||
or helper.helper_npc_id != npc.id
|
||||
or helper.action_id != decision.action_id
|
||||
):
|
||||
return
|
||||
_show_feedback(
|
||||
"Help is on the way",
|
||||
(
|
||||
"%s heard the need and chose to %s."
|
||||
% [npc.npc_name, _get_action_phrase(decision.action_id)]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _on_opportunity_resolved(
|
||||
opportunity: OpportunityStateRecord, resolution_event: EconomicEventRecord
|
||||
) -> void:
|
||||
if opportunity.get_trigger_event_id() != active_trigger_event_id:
|
||||
return
|
||||
var actor_id := int(resolution_event.data["actor_id"])
|
||||
var actor_name := "The player" if actor_id < 0 else _get_npc_name(actor_id)
|
||||
var outcome := (
|
||||
"restocked the pantry"
|
||||
if opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||
else "supplied the woodpile"
|
||||
)
|
||||
_show_feedback("Need met", "%s %s." % [actor_name, outcome])
|
||||
informed_listener_id = -1
|
||||
|
||||
|
||||
func _on_opportunity_invalidated(opportunity: OpportunityStateRecord) -> void:
|
||||
if opportunity.get_trigger_event_id() != active_trigger_event_id:
|
||||
return
|
||||
_show_feedback("Need closed", "The village could no longer act on this report.")
|
||||
informed_listener_id = -1
|
||||
|
||||
|
||||
func _on_state_restored() -> void:
|
||||
_reset_feedback()
|
||||
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
active_trigger_event_id = (
|
||||
opportunity.get_trigger_event_id()
|
||||
if opportunity != null
|
||||
else OpportunityStateRecord.NO_EVENT_ID
|
||||
)
|
||||
informed_listener_id = -1
|
||||
|
||||
|
||||
func _build_need_message(opportunity: OpportunityStateRecord) -> String:
|
||||
var interested_name := _get_npc_name(opportunity.get_interested_npc_id())
|
||||
if opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD:
|
||||
return "%s worries over the empty pantry." % interested_name
|
||||
var trigger: EconomicEventRecord = simulation_manager.get_opportunity_trigger_event(opportunity)
|
||||
var action_name := "work"
|
||||
if trigger != null:
|
||||
action_name = _get_action_phrase(StringName(trigger.data.get("action_id", &"")))
|
||||
return "%s needs wood to finish %s." % [interested_name, action_name]
|
||||
|
||||
|
||||
func _get_need_name(opportunity: OpportunityStateRecord) -> String:
|
||||
return (
|
||||
"empty-pantry worry"
|
||||
if opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||
else "missing-wood report"
|
||||
)
|
||||
|
||||
|
||||
func _get_action_phrase(action_id: StringName) -> String:
|
||||
var definition := SimulationDefinitions.get_action(action_id)
|
||||
return definition.display_name.to_lower() if definition != null else String(action_id)
|
||||
|
||||
|
||||
func _get_npc_name(npc_id: int) -> String:
|
||||
for npc in simulation_manager.npcs:
|
||||
if npc.id == npc_id:
|
||||
return npc.npc_name
|
||||
return "Someone"
|
||||
|
||||
|
||||
func _show_feedback(heading: String, body: String) -> void:
|
||||
feedback_version += 1
|
||||
var version := feedback_version
|
||||
if feedback_tween != null:
|
||||
feedback_tween.kill()
|
||||
kicker.text = heading.to_upper()
|
||||
message.text = body
|
||||
visible = true
|
||||
position = resting_position + Vector2(0.0, ENTER_OFFSET)
|
||||
modulate.a = 0.0
|
||||
accent.pivot_offset = accent.size * 0.5
|
||||
accent.scale.x = 0.15
|
||||
feedback_tween = create_tween().set_parallel(true)
|
||||
feedback_tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
feedback_tween.tween_property(self, "position", resting_position, 0.32)
|
||||
feedback_tween.tween_property(self, "modulate:a", 1.0, 0.24)
|
||||
feedback_tween.tween_property(accent, "scale:x", 1.0, 0.38)
|
||||
_fade_after_delay(version)
|
||||
|
||||
|
||||
func _fade_after_delay(version: int) -> void:
|
||||
await get_tree().create_timer(DISPLAY_SECONDS).timeout
|
||||
if version != feedback_version or not is_inside_tree():
|
||||
return
|
||||
feedback_tween = create_tween().set_parallel(true)
|
||||
feedback_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||
feedback_tween.tween_property(self, "modulate:a", 0.0, 0.45)
|
||||
feedback_tween.tween_property(self, "position:y", resting_position.y - 4.0, 0.45)
|
||||
await feedback_tween.finished
|
||||
if version == feedback_version:
|
||||
visible = false
|
||||
|
||||
|
||||
func _reset_feedback() -> void:
|
||||
feedback_version += 1
|
||||
if feedback_tween != null:
|
||||
feedback_tween.kill()
|
||||
visible = false
|
||||
position = resting_position
|
||||
modulate.a = 0.0
|
||||
if accent != null:
|
||||
accent.scale.x = 1.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwwxux24jc6yq
|
||||
Reference in New Issue
Block a user