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
|
does not recompute decisions. Each idle selection re-derives the capable helper
|
||||||
instead of consulting persisted assignment state. At completion it atomically
|
instead of consulting persisted assignment state. At completion it atomically
|
||||||
pays any definition-backed stored-resource cost before applying the action
|
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
|
effect. A late shortfall suppresses the effect and records a `task_blocked`
|
||||||
manager also captures actor/nearby knowledge before forwarding newly recorded
|
fact. The manager also captures actor/nearby knowledge before forwarding newly
|
||||||
events into `RelationshipSystem`. When an NPC arrives beside a worker at the
|
recorded events into `RelationshipSystem`. When an NPC arrives beside a worker
|
||||||
same non-storage activity site, the manager may transfer one direct known fact
|
at the same non-storage activity site, the manager may transfer one direct
|
||||||
and applies its consequence only to that newly informed listener. It exposes
|
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
|
knowledge, provenance, relationship, and cause queries without moving social
|
||||||
authority into UI. After consequences are known, it protects current causal
|
authority into UI. After consequences are known, it protects current causal
|
||||||
facts, enforces the recent-memory cap, and runs age review from authoritative
|
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;
|
legacy, with a stable source NPC ID for direct communication;
|
||||||
- permits one performed/witnessed fact to cross one co-located social hop, but
|
- permits one performed/witnessed fact to cross one co-located social hop, but
|
||||||
does not relay communicated or ambiguous legacy facts;
|
does not relay communicated or ambiguous legacy facts;
|
||||||
- orders communication by lasting relationship relevance, then acquisition
|
- tries the active opportunity trigger first when the speaker directly knows
|
||||||
recency;
|
it, then orders remaining communication by lasting relationship relevance
|
||||||
|
and acquisition recency;
|
||||||
- retains current relationship causes and at most three other recent facts per
|
- retains current relationship causes and at most three other recent facts per
|
||||||
NPC, expiring day-old recent facts at deterministic review boundaries;
|
NPC, expiring day-old recent facts at deterministic review boundaries;
|
||||||
- captures evidence only when the event happens, never from current positions
|
- 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
|
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
|
||||||
known objective events, immutable acquisition provenance, proximity witnesses
|
known objective events, immutable acquisition provenance, proximity witnesses
|
||||||
at record time, one-hop direct fact transfer, and deterministic recent-fact
|
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
|
- `simulation/relationships/RelationshipSystem.gd` owns directed relationship
|
||||||
queries, event-driven trust changes, and deterministic social tie-breaking;
|
queries, event-driven trust changes, and deterministic social tie-breaking;
|
||||||
- `simulation/opportunities/VillageOpportunitySystem.gd` observes immutable
|
- `simulation/opportunities/VillageOpportunitySystem.gd` observes immutable
|
||||||
@@ -96,8 +97,10 @@ improving ownership.
|
|||||||
retained knowledge. Opportunity presentation is likewise query-only.
|
retained knowledge. Opportunity presentation is likewise query-only.
|
||||||
Relationship cues, interested-villager concern, physical pantry stock, and
|
Relationship cues, interested-villager concern, physical pantry stock, and
|
||||||
refill feedback consume authoritative state or state-change signals and are
|
refill feedback consume authoritative state or state-change signals and are
|
||||||
intentionally absent from saves and checksums. Stable visuals are rebuilt
|
intentionally absent from saves and checksums. The transient village-whisper
|
||||||
from state after restore; transient reactions are not replayed.
|
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
|
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
||||||
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
||||||
- New mutable features define serialization and deterministic continuation at
|
- New mutable features define serialization and deterministic continuation at
|
||||||
|
|||||||
@@ -741,13 +741,18 @@ Completed:
|
|||||||
and sleep/meal precedence. Existing target resolution chooses and reserves
|
and sleep/meal precedence. Existing target resolution chooses and reserves
|
||||||
the finite source, while no quest acceptance, reward, or helper assignment
|
the finite source, while no quest acceptance, reward, or helper assignment
|
||||||
is introduced.
|
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:
|
Next:
|
||||||
|
|
||||||
1. Prove one direct information-to-help chain: transfer the exact open-need
|
1. When no capable helper exists, show one contextual player response hint
|
||||||
trigger at an existing shared activity, then let the newly informed trusted
|
derived from a current player-usable finite source and the real storage
|
||||||
villager be re-derived and respond through the same autonomous selector.
|
target. Clear it as soon as help emerges or the need closes; do not add quest
|
||||||
Keep generic dialogue and multi-hop rumours out of scope, as sequenced in
|
acceptance, waypoint state, or a generic tracker, as sequenced in
|
||||||
`LEARNING_ROADMAP.md`.
|
`LEARNING_ROADMAP.md`.
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
reason. Resolution or invalidation returns the trigger to normal bounded
|
||||||
retention.
|
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
|
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
|
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.
|
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
|
wood need as hunger. The active summary additionally names the currently
|
||||||
derived helper, supply action, trust fact, and inventory or finite-source route,
|
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.
|
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
|
## Deliberate limits
|
||||||
|
|
||||||
|
|||||||
@@ -807,13 +807,27 @@ The first bounded autonomous helper consumer is complete:
|
|||||||
- focused headless and Jajce runtime regressions prove autonomous selection,
|
- focused headless and Jajce runtime regressions prove autonomous selection,
|
||||||
precedence, finite-source targeting, reservation, and restore-time re-query.
|
precedence, finite-source targeting, reservation, and restore-time re-query.
|
||||||
|
|
||||||
Milestone 7 is not complete. The immediate next slice should connect the
|
The first direct information-to-help path is complete:
|
||||||
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
|
- at an eligible shared activity, a speaker who performed or witnessed the
|
||||||
activity, after which the newly informed trusted villager is re-derived as the
|
active need's exact trigger discusses it before other retained direct facts;
|
||||||
helper and responds through the same autonomous selection contract. Keep this
|
- the transfer still references the original event, preserves the speaker and
|
||||||
as a truthful event-reference transfer, not generic dialogue or multi-hop
|
direct source method, stops after one hop, and creates no dialogue/event copy;
|
||||||
rumour infrastructure.
|
- 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:
|
Recently completed:
|
||||||
|
|
||||||
|
|||||||
+20
-7
@@ -472,7 +472,9 @@ follow camera.
|
|||||||
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.
|
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
|
## Current runtime flow
|
||||||
|
|
||||||
@@ -556,6 +558,7 @@ NpcVisual navigates through the active world
|
|||||||
│ ├── knowledge_retention_consequence_test.gd
|
│ ├── knowledge_retention_consequence_test.gd
|
||||||
│ ├── jajce_runtime_integration_test.gd
|
│ ├── jajce_runtime_integration_test.gd
|
||||||
│ ├── npc_visual_lifecycle_test.gd
|
│ ├── npc_visual_lifecycle_test.gd
|
||||||
|
│ ├── opportunity_communication_helper_test.gd
|
||||||
│ ├── relationship_consequence_test.gd
|
│ ├── relationship_consequence_test.gd
|
||||||
│ ├── resource_node_player_parity_test.gd
|
│ ├── resource_node_player_parity_test.gd
|
||||||
│ ├── simulation_definitions_test.gd
|
│ ├── simulation_definitions_test.gd
|
||||||
@@ -579,7 +582,9 @@ NpcVisual navigates through the active world
|
|||||||
│ │ └── ResourceNode.tscn
|
│ │ └── ResourceNode.tscn
|
||||||
│ ├── active_world_adapter.gd
|
│ ├── active_world_adapter.gd
|
||||||
│ ├── world_view_manager.gd
|
│ ├── world_view_manager.gd
|
||||||
│ └── ui/ui.gd
|
│ └── ui/
|
||||||
|
│ ├── ui.gd
|
||||||
|
│ └── village_whisper_hud.gd
|
||||||
├── main.tscn
|
├── main.tscn
|
||||||
└── project.godot
|
└── 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
|
quest acceptance, reward, or opportunity-specific event is persisted; ordinary
|
||||||
task-start history remains unchanged.
|
task-start history remains unchanged.
|
||||||
|
|
||||||
Milestone 7 remains in progress. The immediate next slice should prove the
|
The first direct information-to-help path is complete. At shared activity, a
|
||||||
combined information-to-help path: communicate an open need's exact trigger
|
performer or direct witness now discusses the active need's exact trigger before
|
||||||
once at a real shared activity, then re-derive the newly informed trusted NPC as
|
newer unrelated facts. The original event and first-source provenance remain
|
||||||
the autonomous helper. Keep the transfer direct and source-backed rather than
|
authoritative and one-hop. A newly informed trusted, supplied villager is then
|
||||||
introducing generic dialogue or multi-hop rumours.
|
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:
|
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
|
The helper result remains absent: restore retains any in-progress ordinary task
|
||||||
and independently re-derives current capability from restored facts.
|
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
|
## Resource authority
|
||||||
|
|
||||||
`SimulationManager` owns `ResourceStateRecord` instances independently of the
|
`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://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="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"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
||||||
radius = 0.4
|
radius = 0.4
|
||||||
@@ -126,6 +127,66 @@ offset_right = 940.0
|
|||||||
offset_bottom = 66.0
|
offset_bottom = 66.0
|
||||||
script = ExtResource("12_timedial")
|
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="."]
|
[node name="CrisisCaptionLayer" type="CanvasLayer" parent="."]
|
||||||
layer = 4
|
layer = 4
|
||||||
|
|
||||||
|
|||||||
@@ -127,11 +127,9 @@ func generate_npcs() -> void:
|
|||||||
npc_random.randf_range(1.0, 10.0),
|
npc_random.randf_range(1.0, 10.0),
|
||||||
npc_random
|
npc_random
|
||||||
)
|
)
|
||||||
|
|
||||||
npc.debug_logs = debug_logs
|
npc.debug_logs = debug_logs
|
||||||
npcs.append(npc)
|
npcs.append(npc)
|
||||||
wander_random_sources[i] = _create_random_source(i, 1)
|
wander_random_sources[i] = _create_random_source(i, 1)
|
||||||
|
|
||||||
var home_count := home_positions.size()
|
var home_count := home_positions.size()
|
||||||
if home_count > 0:
|
if home_count > 0:
|
||||||
for i in range(npcs.size()):
|
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):
|
if not _can_communicate_at_shared_activity(speaker, listener):
|
||||||
return false
|
return false
|
||||||
var known_event_ids := event_knowledge_system.get_communicable_event_ids(
|
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:
|
for event_id in known_event_ids:
|
||||||
var event := event_log.get_by_id(event_id)
|
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)
|
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]:
|
func get_communicable_event_ids(
|
||||||
return _get_ranked_event_ids(knower_id, lasting_event_ids, true, 0)
|
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(
|
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
|
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:
|
func get_latest() -> OpportunityStateRecord:
|
||||||
var sorted := get_all_sorted()
|
var sorted := get_all_sorted()
|
||||||
return sorted[-1] if not sorted.is_empty() else null
|
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"
|
"Restarting the transient trust blossom must not mutate simulation state"
|
||||||
)
|
)
|
||||||
var village_ui := main_scene.get_node("UI")
|
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.selected_npc_index = witness.id
|
||||||
village_ui.call("_refresh_npc_inspector")
|
village_ui.call("_refresh_npc_inspector")
|
||||||
_check(
|
_check(
|
||||||
@@ -250,8 +259,9 @@ func _run() -> void:
|
|||||||
and pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.EMPTY
|
and pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.EMPTY
|
||||||
and pantry_visual.get_node("EmptyStock").visible
|
and pantry_visual.get_node("EmptyStock").visible
|
||||||
and not pantry_visual.get_node("RefillResponse").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 player := main_scene.get_node("Player") as Node3D
|
||||||
var opportunity_bush := (
|
var opportunity_bush := (
|
||||||
@@ -270,8 +280,10 @@ func _run() -> void:
|
|||||||
player_opportunity.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
player_opportunity.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
||||||
and "Resolved village need" in inspector_label.text
|
and "Resolved village need" in inspector_label.text
|
||||||
and "◆ Player restocked the pantry" 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()
|
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
|
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile") as StorageNode
|
||||||
)
|
)
|
||||||
contributor.position = woodpile_node.get_interaction_position()
|
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
|
witness.profession = SimulationIds.PROFESSION_WOODCUTTER
|
||||||
contributor.set_task(SimulationIds.ACTION_STUDY, 1.0)
|
contributor.set_task(SimulationIds.ACTION_STUDY, 1.0)
|
||||||
contributor.target_id = SimulationIds.STORAGE_VILLAGE_WOODPILE
|
contributor.target_id = SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||||
@@ -292,14 +306,64 @@ func _run() -> void:
|
|||||||
simulation_manager.simulate_tick()
|
simulation_manager.simulate_tick()
|
||||||
village_ui.selected_npc_index = contributor.id
|
village_ui.selected_npc_index = contributor.id
|
||||||
village_ui.call("_refresh_npc_inspector")
|
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()
|
var runtime_helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
||||||
_check(
|
_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.helper_npc_id == witness.id
|
||||||
and runtime_helper.action_id == SimulationIds.ACTION_GATHER_WOOD
|
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 := (
|
var runtime_helper_reason := (
|
||||||
"Knows the need; trust %.2f toward %s" % [runtime_helper.trust, contributor.npc_name]
|
"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 "◆ Find wood for Study" in inspector_label.text
|
||||||
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
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.current_task = SimulationIds.ACTION_IDLE
|
||||||
witness.task_state = SimNPC.TASK_STATE_IDLE
|
witness.task_state = SimNPC.TASK_STATE_IDLE
|
||||||
@@ -338,9 +402,19 @@ func _run() -> void:
|
|||||||
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
|
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
|
||||||
and helper_resource != null
|
and helper_resource != null
|
||||||
and helper_resource.get_reserved_by() == witness.id
|
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)
|
contributor.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0)
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(
|
is_equal_approx(
|
||||||
@@ -354,8 +428,10 @@ func _run() -> void:
|
|||||||
(
|
(
|
||||||
"Village need" not in village_stats_label.text
|
"Village need" not in village_stats_label.text
|
||||||
and "◆ %s supplied the woodpile" % contributor.npc_name in inspector_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(
|
_check(
|
||||||
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
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(
|
func _on_relationship_changed(
|
||||||
_relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord
|
_relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord
|
||||||
) -> void:
|
) -> void:
|
||||||
|
_refresh_village_display()
|
||||||
_refresh_npc_inspector()
|
_refresh_npc_inspector()
|
||||||
|
|
||||||
|
|
||||||
func _on_event_knowledge_changed(_knower_id: int, _event: EconomicEventRecord) -> void:
|
func _on_event_knowledge_changed(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||||
|
_refresh_village_display()
|
||||||
_refresh_npc_inspector()
|
_refresh_npc_inspector()
|
||||||
|
|
||||||
|
|
||||||
func _on_event_knowledge_forgotten(_knower_id: int, _event: EconomicEventRecord) -> void:
|
func _on_event_knowledge_forgotten(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||||
|
_refresh_village_display()
|
||||||
_refresh_npc_inspector()
|
_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