feat: add person history and trust reactions
This commit is contained in:
@@ -19,7 +19,7 @@ SimulationClock
|
|||||||
-> RelationshipSystem applies evidence-gated social consequences
|
-> RelationshipSystem applies evidence-gated social consequences
|
||||||
-> WorldViewManager presents travel and NPC state
|
-> WorldViewManager presents travel and NPC state
|
||||||
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
||||||
-> NpcVisual performs local navigation and animation
|
-> NpcVisual performs local navigation, animation, and transient reactions
|
||||||
```
|
```
|
||||||
|
|
||||||
`SimulationManager` is the scene-tree façade for the simulation. It owns the
|
`SimulationManager` is the scene-tree façade for the simulation. It owns the
|
||||||
@@ -35,7 +35,7 @@ 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;
|
retention/importance ranking;
|
||||||
- `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/persistence/` owns save-slot file safety;
|
- `simulation/persistence/` owns save-slot file safety;
|
||||||
@@ -81,6 +81,9 @@ improving ownership.
|
|||||||
- Presentation may report facts and submit commands; it does not choose NPC
|
- Presentation may report facts and submit commands; it does not choose NPC
|
||||||
actions or own resource, storage, inventory, event, knowledge, relationship,
|
actions or own resource, storage, inventory, event, knowledge, relationship,
|
||||||
or reservation state.
|
or reservation state.
|
||||||
|
- Inspector history is a read-only façade query over objective events and
|
||||||
|
retained knowledge. Relationship cues consume state-change signals and are
|
||||||
|
intentionally absent from saves and checksums.
|
||||||
- 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
|
||||||
|
|||||||
@@ -700,11 +700,16 @@ Completed:
|
|||||||
recent facts. A deterministic daily-sized review fades old routine facts,
|
recent facts. A deterministic daily-sized review fades old routine facts,
|
||||||
conversations prefer lasting evidence, and the inspector shows both memory
|
conversations prefer lasting evidence, and the inspector shows both memory
|
||||||
counts and the displayed fact's retention class.
|
counts and the displayed fact's retention class.
|
||||||
|
24. Person history and trust blossom: the selected-NPC inspector now separates
|
||||||
|
up to four lasting-first retained memories from three objective personal
|
||||||
|
actions. A real evidence-caused trust gain produces one short amber blossom
|
||||||
|
above its observer, including in cinematic mode, without persistent or
|
||||||
|
simulation-owned animation state.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. Add a compact retained-memory/person-history view and one visible social
|
1. Implement one bounded Milestone 7 rumour/opportunity proof, as sequenced in
|
||||||
reaction cue, 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
|
||||||
mechanics. The next proof is a beautiful stage for the systems that already
|
mechanics. The next proof is a beautiful stage for the systems that already
|
||||||
|
|||||||
@@ -87,8 +87,11 @@ save/restore continuity.
|
|||||||
`NpcVisual` shows a small food sack whenever its authoritative inventory
|
`NpcVisual` shows a small food sack whenever its authoritative inventory
|
||||||
contains food. A newly loaded visual derives the same state directly from the
|
contains food. A newly loaded visual derives the same state directly from the
|
||||||
NPC record, so unloading presentation does not lose the fact.
|
NPC record, so unloading presentation does not lose the fact.
|
||||||
The selected-NPC inspector shows lasting/recent counts and labels the displayed
|
The selected-NPC inspector shows up to four lasting-first retained facts with
|
||||||
fact with both acquisition provenance and retention class.
|
acquisition provenance, separately from three objective events the NPC
|
||||||
|
personally performed. A real trust-changing consequence emits one transient
|
||||||
|
amber blossom above its observer. The cue is presentation-only, remains visible
|
||||||
|
in cinematic mode, and is neither saved nor replayed after visual rebuild.
|
||||||
|
|
||||||
## Deliberate limits
|
## Deliberate limits
|
||||||
|
|
||||||
|
|||||||
@@ -698,11 +698,22 @@ The first importance/retention slice is complete:
|
|||||||
boundary, capacity, cause-protection, save/checksum, and informed-choice
|
boundary, capacity, cause-protection, save/checksum, and informed-choice
|
||||||
regressions cover the slice.
|
regressions cover the slice.
|
||||||
|
|
||||||
This advances the first Milestone 6 evidence-to-choice path without claiming
|
The compact history/reaction slice is complete:
|
||||||
full belief simulation. The practical next sequence is a compact retained
|
|
||||||
memory/person-history view and a visible social-reaction cue, then one bounded
|
- the selected-person inspector separates lasting-first retained memories from
|
||||||
Milestone 7 rumour/opportunity proof without generalized distortion or
|
the NPC's own objective event history and keeps both lists bounded;
|
||||||
free-form dialogue.
|
- remembered facts show performed, witnessed, communicated, or legacy
|
||||||
|
provenance without copying authoritative events into presentation state;
|
||||||
|
- a real trust change blooms once above its observer, remains visible in
|
||||||
|
cinematic mode, and is never persisted or replayed after visual rebuild;
|
||||||
|
- ranking, forgetting/objective-history separation, observer-only routing,
|
||||||
|
checksum neutrality, restore, cinematic, and death regressions cover the
|
||||||
|
slice.
|
||||||
|
|
||||||
|
This completes the first bounded Milestone 6 evidence-to-choice presentation
|
||||||
|
path without claiming full belief simulation. The practical next slice is one
|
||||||
|
bounded Milestone 7 rumour/opportunity proof without generalized distortion,
|
||||||
|
free-form dialogue, or a generic quest framework.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
|
|||||||
+17
-12
@@ -373,13 +373,13 @@ A small village panel displays:
|
|||||||
- a Tab-cycled NPC inspector with profession, needs, task state, destination,
|
- a Tab-cycled NPC inspector with profession, needs, task state, destination,
|
||||||
decision reason, utility scores, directed familiarity/trust, and the exact
|
decision reason, utility scores, directed familiarity/trust, and the exact
|
||||||
completed event that last changed trust;
|
completed event that last changed trust;
|
||||||
- the selected NPC's latest known objective fact, resolved from its own
|
- a compact selected-person history that separates up to four importance-ranked
|
||||||
persisted knowledge rather than omniscient event history.
|
retained memories from the NPC's three latest objective personal actions.
|
||||||
|
|
||||||
NPC name/profession labels, definition-driven colors and props, carried-food
|
NPC name/profession labels, definition-driven colors and props, carried-food
|
||||||
visuals, and compact task glyphs make active simulation state readable in the
|
visuals, compact task glyphs, and a brief amber blossom on real trust gains make
|
||||||
world. The F10 cinematic mode hides debug labels while preserving the task
|
active simulation state readable in the world. The F10 cinematic mode hides
|
||||||
glyphs.
|
debug labels while preserving task glyphs and transient social reactions.
|
||||||
|
|
||||||
## Runtime architecture
|
## Runtime architecture
|
||||||
|
|
||||||
@@ -416,7 +416,8 @@ Focused `RefCounted` collaborators keep rule ownership visible:
|
|||||||
- `SimulationEventLog` owns deterministic event history and queries;
|
- `SimulationEventLog` owns deterministic event history and queries;
|
||||||
- `EventKnowledgeSystem` owns per-NPC known-event references, immutable
|
- `EventKnowledgeSystem` owns per-NPC known-event references, immutable
|
||||||
first-acquisition time/provenance, actor/nearby evidence capture, bounded
|
first-acquisition time/provenance, actor/nearby evidence capture, bounded
|
||||||
one-hop communication, and deterministic recent-memory retention;
|
one-hop communication, deterministic recent-memory retention, and stable
|
||||||
|
importance ranking for presentation/communication queries;
|
||||||
- `RelationshipSystem` owns directed relationship state, event-driven trust
|
- `RelationshipSystem` owns directed relationship state, event-driven trust
|
||||||
consequences, and deterministic social queries.
|
consequences, and deterministic social queries.
|
||||||
|
|
||||||
@@ -432,7 +433,9 @@ than duplicating these responsibilities across additional manager nodes.
|
|||||||
- sends resolved travel destinations to visuals;
|
- sends resolved travel destinations to visuals;
|
||||||
- reports arrival and navigation failure back to `SimulationManager`;
|
- reports arrival and navigation failure back to `SimulationManager`;
|
||||||
- synchronizes successful visual movement into authoritative NPC position;
|
- synchronizes successful visual movement into authoritative NPC position;
|
||||||
- applies visual death state.
|
- applies visual death state;
|
||||||
|
- turns authoritative relationship-change signals into observer-only transient
|
||||||
|
presentation cues without queuing or persisting them.
|
||||||
|
|
||||||
### `player/npc/NpcVisual.gd`
|
### `player/npc/NpcVisual.gd`
|
||||||
|
|
||||||
@@ -443,7 +446,7 @@ than duplicating these responsibilities across additional manager nodes.
|
|||||||
- moves and rotates toward path points;
|
- moves and rotates toward path points;
|
||||||
- reports arrival;
|
- reports arrival;
|
||||||
- presents definition-driven profession silhouettes, carried resources, task
|
- presents definition-driven profession silhouettes, carried resources, task
|
||||||
glyphs, walk motion, and death state.
|
glyphs, walk motion, brief trust-gain blossoms, and death state.
|
||||||
|
|
||||||
### `player/player.gd` and `player/camera_rig.gd`
|
### `player/player.gd` and `player/camera_rig.gd`
|
||||||
|
|
||||||
@@ -593,8 +596,8 @@ These are expected prototype constraints, not necessarily isolated bugs:
|
|||||||
evidence, personalized reinforcement/decay, false beliefs, or multi-hop
|
evidence, personalized reinforcement/decay, false beliefs, or multi-hop
|
||||||
rumours.
|
rumours.
|
||||||
- The reason inspector exposes current decisions, utility rejections, one exact
|
- The reason inspector exposes current decisions, utility rejections, one exact
|
||||||
relationship cause, memory counts, and the latest known fact with its speaker
|
relationship cause, and a compact person-history view that distinguishes
|
||||||
and retention class, but a broader person-history view is not yet present.
|
importance-ranked retained memories from objective personal actions.
|
||||||
- Active navigation is used as if all agents are local; no simulation LOD exists.
|
- Active navigation is used as if all agents are local; no simulation LOD exists.
|
||||||
- Unloaded traveling NPCs preserve their state but do not yet advance through
|
- Unloaded traveling NPCs preserve their state but do not yet advance through
|
||||||
abstract travel time.
|
abstract travel time.
|
||||||
@@ -841,8 +844,10 @@ and relationship cause, the inspector names the speaker, and the listener's
|
|||||||
future work can change without duplicating event history. Importance and
|
future work can change without duplicating event history. Importance and
|
||||||
retention now keep current relationship causes lasting, cap other memories,
|
retention now keep current relationship causes lasting, cap other memories,
|
||||||
and forget routine facts at deterministic daily-sized reviews without deleting
|
and forget routine facts at deterministic daily-sized reviews without deleting
|
||||||
the objective event. A compact retained-memory/person-history view and visible
|
the objective event. The selected-person inspector now shows lasting-first
|
||||||
social-reaction cue are the next systems slice.
|
retained memories beside objective personal history, and real trust changes
|
||||||
|
produce a short observer-only amber blossom without altering saved state. The
|
||||||
|
next systems slice is one bounded rumour/opportunity proof.
|
||||||
|
|
||||||
The remaining simulation-garden target still aims for:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Simulation Garden 01
|
# Simulation Garden 01
|
||||||
|
|
||||||
Captured: 2026-07-11
|
Captured: 2026-07-12
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
@@ -29,8 +29,10 @@ $env:LOCALAPPDATA = 'C:\Users\Rijad\Documents\Simulation Game\logs\quality\godot
|
|||||||
```
|
```
|
||||||
|
|
||||||
The script warms up `main.tscn`, verifies that the simulation and active NPC
|
The script warms up `main.tscn`, verifies that the simulation and active NPC
|
||||||
visuals exist, captures the F10 debug presentation, then captures the cinematic
|
visuals exist, stages a real food-deposit/trust consequence plus one wind gust,
|
||||||
presentation with development labels and UI hidden.
|
captures the F10 debug presentation, then captures the cinematic presentation
|
||||||
|
with development labels and UI hidden. The trust blossom is restarted only for
|
||||||
|
the second framing so both captures show the same real consequence clearly.
|
||||||
|
|
||||||
## Review
|
## Review
|
||||||
|
|
||||||
@@ -83,11 +85,9 @@ share the prevailing direction used by deterministic tree-canopy motion and
|
|||||||
chimney smoke. The capture tool stages one gust so the next visual comparison
|
chimney smoke. The capture tool stages one gust so the next visual comparison
|
||||||
does not randomly land during the intended quiet interval.
|
does not randomly land during the intended quiet interval.
|
||||||
|
|
||||||
The PNGs listed above still predate that gust-controller pass. A fresh Metal
|
The PNGs listed above have now been refreshed on the Metal renderer and include
|
||||||
capture was unavailable when the code and headless mesh/shader checks landed,
|
the staged mid-sweep gust, so they can be used to assess ribbon opacity and
|
||||||
so do not use those images as evidence of the final ribbon opacity or terrain
|
terrain contact.
|
||||||
contact. Replace both images after running the capture command on the normal
|
|
||||||
renderer and reviewing the staged mid-sweep gust.
|
|
||||||
|
|
||||||
The day/night controller also reconnects to the authoritative simulation clock
|
The day/night controller also reconnects to the authoritative simulation clock
|
||||||
after scene startup and uses the correct sunrise/sunset color order, preventing
|
after scene startup and uses the correct sunrise/sunset color order, preventing
|
||||||
@@ -99,3 +99,13 @@ and the elevated staggered follow camera are unchanged. The next visible gains
|
|||||||
should come from replacing remaining blockout architecture, strengthening work
|
should come from replacing remaining blockout architecture, strengthening work
|
||||||
props, and improving terrain/color hierarchy—not from changing the camera
|
props, and improving terrain/color hierarchy—not from changing the camera
|
||||||
language again.
|
language again.
|
||||||
|
|
||||||
|
## July 12 social-history follow-up
|
||||||
|
|
||||||
|
The debug capture now stages Tarik's real witnessed food-deposit memory and
|
||||||
|
trust consequence. The inspector distinguishes that lasting subjective memory
|
||||||
|
from Tarik's own objective actions in a shorter, more legible panel. The same
|
||||||
|
consequence produces one brief amber halo-and-mote blossom above Tarik; it
|
||||||
|
remains visible when F10 removes the debug UI but does not become permanent
|
||||||
|
world clutter. Both refreshed images preserve the established elevated,
|
||||||
|
slightly staggered top-down camera.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 418 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 446 KiB |
@@ -4,6 +4,9 @@ signal arrived_at_target(sim_id: int)
|
|||||||
signal navigation_failed(sim_id: int)
|
signal navigation_failed(sim_id: int)
|
||||||
signal position_changed(sim_id: int, active_position: Vector3)
|
signal position_changed(sim_id: int, active_position: Vector3)
|
||||||
|
|
||||||
|
const TRUST_REACTION_BASE_POSITION := Vector3(0.0, 2.72, 0.0)
|
||||||
|
const TRUST_REACTION_START_SCALE := Vector3(0.24, 0.24, 0.24)
|
||||||
|
|
||||||
@export var debug_logs := false
|
@export var debug_logs := false
|
||||||
|
|
||||||
@export var move_speed := 3.0
|
@export var move_speed := 3.0
|
||||||
@@ -25,6 +28,13 @@ signal position_changed(sim_id: int, active_position: Vector3)
|
|||||||
@onready var carried_wood_visual: Node3D = $CarriedWood
|
@onready var carried_wood_visual: Node3D = $CarriedWood
|
||||||
@onready var task_glyph_root: Node3D = $TaskGlyphRoot
|
@onready var task_glyph_root: Node3D = $TaskGlyphRoot
|
||||||
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
|
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
|
||||||
|
@onready var trust_reaction_root: Node3D = $TrustReactionRoot
|
||||||
|
@onready var trust_reaction_visuals: Array[MeshInstance3D] = [
|
||||||
|
$TrustReactionRoot/Halo,
|
||||||
|
$TrustReactionRoot/MoteLeft,
|
||||||
|
$TrustReactionRoot/MoteCenter,
|
||||||
|
$TrustReactionRoot/MoteRight,
|
||||||
|
]
|
||||||
|
|
||||||
var has_reported_arrival := false
|
var has_reported_arrival := false
|
||||||
var sim_id: int = -1
|
var sim_id: int = -1
|
||||||
@@ -44,6 +54,7 @@ var dead_material := StandardMaterial3D.new()
|
|||||||
var debug_overlay_visible := true
|
var debug_overlay_visible := true
|
||||||
var walk_phase := 0.0
|
var walk_phase := 0.0
|
||||||
var glyph_phase := 0.0
|
var glyph_phase := 0.0
|
||||||
|
var trust_reaction_tween: Tween
|
||||||
|
|
||||||
|
|
||||||
func debug_log(message: String) -> void:
|
func debug_log(message: String) -> void:
|
||||||
@@ -53,6 +64,7 @@ func debug_log(message: String) -> void:
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
dead_material.albedo_color = Color(0.25, 0.25, 0.25, 1.0)
|
dead_material.albedo_color = Color(0.25, 0.25, 0.25, 1.0)
|
||||||
|
_stop_trust_gain_reaction()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
@@ -91,6 +103,7 @@ func setup_from_sim(npc: SimNPC) -> void:
|
|||||||
npc_name = npc.npc_name
|
npc_name = npc.npc_name
|
||||||
profession = npc.profession
|
profession = npc.profession
|
||||||
name = "NPC_%s_%s" % [sim_id, npc_name]
|
name = "NPC_%s_%s" % [sim_id, npc_name]
|
||||||
|
_stop_trust_gain_reaction()
|
||||||
_apply_personal_details()
|
_apply_personal_details()
|
||||||
_apply_profession_presentation()
|
_apply_profession_presentation()
|
||||||
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)
|
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)
|
||||||
@@ -196,6 +209,67 @@ func set_debug_overlay_visible(is_visible: bool) -> void:
|
|||||||
profession_label.visible = is_visible and not is_dead_visual
|
profession_label.visible = is_visible and not is_dead_visual
|
||||||
|
|
||||||
|
|
||||||
|
func play_trust_gain_reaction() -> void:
|
||||||
|
if is_dead_visual or trust_reaction_root == null:
|
||||||
|
return
|
||||||
|
_stop_trust_gain_reaction()
|
||||||
|
trust_reaction_root.visible = true
|
||||||
|
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION
|
||||||
|
trust_reaction_root.rotation = Vector3.ZERO
|
||||||
|
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
||||||
|
_set_trust_reaction_transparency(1.0)
|
||||||
|
|
||||||
|
trust_reaction_tween = create_tween()
|
||||||
|
trust_reaction_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
|
||||||
|
trust_reaction_tween.tween_property(trust_reaction_root, "scale", Vector3.ONE, 0.22)
|
||||||
|
trust_reaction_tween.parallel().tween_method(_set_trust_reaction_transparency, 1.0, 0.0, 0.22)
|
||||||
|
trust_reaction_tween.tween_property(
|
||||||
|
trust_reaction_root,
|
||||||
|
"position",
|
||||||
|
TRUST_REACTION_BASE_POSITION + Vector3(0.0, 0.16, 0.0),
|
||||||
|
0.68
|
||||||
|
)
|
||||||
|
trust_reaction_tween.parallel().tween_property(trust_reaction_root, "rotation:y", 0.32, 0.68)
|
||||||
|
trust_reaction_tween.tween_property(
|
||||||
|
trust_reaction_root,
|
||||||
|
"position",
|
||||||
|
TRUST_REACTION_BASE_POSITION + Vector3(0.0, 0.44, 0.0),
|
||||||
|
0.55
|
||||||
|
)
|
||||||
|
trust_reaction_tween.parallel().tween_property(
|
||||||
|
trust_reaction_root, "scale", Vector3.ONE * 1.16, 0.55
|
||||||
|
)
|
||||||
|
trust_reaction_tween.parallel().tween_method(_set_trust_reaction_transparency, 0.0, 1.0, 0.55)
|
||||||
|
trust_reaction_tween.tween_callback(_finish_trust_gain_reaction)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_trust_reaction_transparency(value: float) -> void:
|
||||||
|
for visual in trust_reaction_visuals:
|
||||||
|
visual.transparency = clampf(value, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish_trust_gain_reaction() -> void:
|
||||||
|
trust_reaction_root.visible = false
|
||||||
|
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION
|
||||||
|
trust_reaction_root.rotation = Vector3.ZERO
|
||||||
|
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
||||||
|
_set_trust_reaction_transparency(1.0)
|
||||||
|
trust_reaction_tween = null
|
||||||
|
|
||||||
|
|
||||||
|
func _stop_trust_gain_reaction() -> void:
|
||||||
|
if trust_reaction_tween != null:
|
||||||
|
trust_reaction_tween.kill()
|
||||||
|
trust_reaction_tween = null
|
||||||
|
if trust_reaction_root == null:
|
||||||
|
return
|
||||||
|
trust_reaction_root.visible = false
|
||||||
|
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION
|
||||||
|
trust_reaction_root.rotation = Vector3.ZERO
|
||||||
|
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
||||||
|
_set_trust_reaction_transparency(1.0)
|
||||||
|
|
||||||
|
|
||||||
func set_target_position(pos: Vector3) -> void:
|
func set_target_position(pos: Vector3) -> void:
|
||||||
path_request_id += 1
|
path_request_id += 1
|
||||||
var request_id := path_request_id
|
var request_id := path_request_id
|
||||||
@@ -324,6 +398,7 @@ func _report_navigation_failure_once() -> void:
|
|||||||
|
|
||||||
func apply_dead_visual_state() -> void:
|
func apply_dead_visual_state() -> void:
|
||||||
is_dead_visual = true
|
is_dead_visual = true
|
||||||
|
_stop_trust_gain_reaction()
|
||||||
carried_food_visual.visible = false
|
carried_food_visual.visible = false
|
||||||
carried_wood_visual.visible = false
|
carried_wood_visual.visible = false
|
||||||
profession_prop.visible = false
|
profession_prop.visible = false
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=18 format=3 uid="uid://dhxxyprqflotq"]
|
[gd_scene load_steps=21 format=3 uid="uid://dhxxyprqflotq"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bha8uf40p3sa0" path="res://player/npc/NpcVisual.gd" id="1_ixfoq"]
|
[ext_resource type="Script" uid="uid://bha8uf40p3sa0" path="res://player/npc/NpcVisual.gd" id="1_ixfoq"]
|
||||||
|
|
||||||
@@ -84,6 +84,28 @@ top_radius = 0.09
|
|||||||
bottom_radius = 0.11
|
bottom_radius = 0.11
|
||||||
height = 0.62
|
height = 0.62
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_trust_reaction"]
|
||||||
|
transparency = 1
|
||||||
|
shading_mode = 0
|
||||||
|
albedo_color = Color(1, 0.67, 0.24, 0.9)
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(1, 0.52, 0.16, 1)
|
||||||
|
emission_energy_multiplier = 0.45
|
||||||
|
|
||||||
|
[sub_resource type="TorusMesh" id="TorusMesh_trust_reaction"]
|
||||||
|
material = SubResource("StandardMaterial3D_trust_reaction")
|
||||||
|
inner_radius = 0.17
|
||||||
|
outer_radius = 0.26
|
||||||
|
rings = 16
|
||||||
|
ring_segments = 8
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="SphereMesh_trust_reaction"]
|
||||||
|
material = SubResource("StandardMaterial3D_trust_reaction")
|
||||||
|
radius = 0.075
|
||||||
|
height = 0.14
|
||||||
|
radial_segments = 8
|
||||||
|
rings = 4
|
||||||
|
|
||||||
[node name="NpcVisual" type="CharacterBody3D"]
|
[node name="NpcVisual" type="CharacterBody3D"]
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
@@ -183,6 +205,31 @@ position = Vector3(0, 2.35, 0)
|
|||||||
|
|
||||||
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
||||||
|
|
||||||
|
[node name="TrustReactionRoot" type="Node3D" parent="."]
|
||||||
|
visible = false
|
||||||
|
position = Vector3(0, 2.72, 0)
|
||||||
|
scale = Vector3(0.24, 0.24, 0.24)
|
||||||
|
|
||||||
|
[node name="Halo" type="MeshInstance3D" parent="TrustReactionRoot"]
|
||||||
|
mesh = SubResource("TorusMesh_trust_reaction")
|
||||||
|
|
||||||
|
[node name="MoteLeft" type="MeshInstance3D" parent="TrustReactionRoot"]
|
||||||
|
position = Vector3(-0.22, 0.08, 0)
|
||||||
|
rotation = Vector3(0, 0, -0.436332)
|
||||||
|
scale = Vector3(0.72, 1.25, 0.72)
|
||||||
|
mesh = SubResource("SphereMesh_trust_reaction")
|
||||||
|
|
||||||
|
[node name="MoteCenter" type="MeshInstance3D" parent="TrustReactionRoot"]
|
||||||
|
position = Vector3(0, 0.19, 0)
|
||||||
|
scale = Vector3(0.8, 1.35, 0.8)
|
||||||
|
mesh = SubResource("SphereMesh_trust_reaction")
|
||||||
|
|
||||||
|
[node name="MoteRight" type="MeshInstance3D" parent="TrustReactionRoot"]
|
||||||
|
position = Vector3(0.22, 0.08, 0)
|
||||||
|
rotation = Vector3(0, 0, 0.436332)
|
||||||
|
scale = Vector3(0.72, 1.25, 0.72)
|
||||||
|
mesh = SubResource("SphereMesh_trust_reaction")
|
||||||
|
|
||||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
||||||
path_desired_distance = 0.3
|
path_desired_distance = 0.3
|
||||||
target_desired_distance = 0.5
|
target_desired_distance = 0.5
|
||||||
|
|||||||
@@ -730,6 +730,18 @@ func get_known_events(npc_id: int, max_count: int = 3) -> Array[EconomicEventRec
|
|||||||
return known
|
return known
|
||||||
|
|
||||||
|
|
||||||
|
func get_retained_memory_events(npc_id: int, max_count: int = 4) -> Array[EconomicEventRecord]:
|
||||||
|
var retained: Array[EconomicEventRecord] = []
|
||||||
|
var event_ids := event_knowledge_system.get_retained_event_ids(
|
||||||
|
npc_id, _get_lasting_event_ids(npc_id), max_count
|
||||||
|
)
|
||||||
|
for event_id in event_ids:
|
||||||
|
var event := event_log.get_by_id(event_id)
|
||||||
|
if event != null:
|
||||||
|
retained.append(event)
|
||||||
|
return retained
|
||||||
|
|
||||||
|
|
||||||
func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
||||||
return event_knowledge_system.knows_event(npc_id, event_id)
|
return event_knowledge_system.knows_event(npc_id, event_id)
|
||||||
|
|
||||||
|
|||||||
@@ -103,12 +103,30 @@ func get_known_event_ids(knower_id: int, max_count: int = 3) -> Array[int]:
|
|||||||
return recent_ids
|
return recent_ids
|
||||||
|
|
||||||
|
|
||||||
|
func get_retained_event_ids(
|
||||||
|
knower_id: int, lasting_event_ids: Array[int], max_count: int = 4
|
||||||
|
) -> Array[int]:
|
||||||
|
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(knower_id: int, lasting_event_ids: Array[int]) -> Array[int]:
|
||||||
|
return _get_ranked_event_ids(knower_id, lasting_event_ids, true, 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_ranked_event_ids(
|
||||||
|
knower_id: int, lasting_event_ids: Array[int], direct_only: bool, max_count: int
|
||||||
|
) -> Array[int]:
|
||||||
var ranked: Array[Dictionary] = []
|
var ranked: Array[Dictionary] = []
|
||||||
for record in known_events.values():
|
for record in known_events.values():
|
||||||
if record.get_knower_id() != knower_id:
|
if record.get_knower_id() != knower_id:
|
||||||
continue
|
continue
|
||||||
if record.get_acquisition_method() not in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS:
|
if (
|
||||||
|
direct_only
|
||||||
|
and (
|
||||||
|
record.get_acquisition_method()
|
||||||
|
not in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS
|
||||||
|
)
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
(
|
(
|
||||||
ranked
|
ranked
|
||||||
@@ -120,10 +138,12 @@ func get_communicable_event_ids(knower_id: int, lasting_event_ids: Array[int]) -
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ranked.sort_custom(_sort_communicable)
|
ranked.sort_custom(_sort_by_importance)
|
||||||
var event_ids: Array[int] = []
|
var event_ids: Array[int] = []
|
||||||
for item in ranked:
|
for item in ranked:
|
||||||
event_ids.append(int(item["event_id"]))
|
event_ids.append(int(item["event_id"]))
|
||||||
|
if max_count > 0 and event_ids.size() >= max_count:
|
||||||
|
break
|
||||||
return event_ids
|
return event_ids
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +254,7 @@ static func _sort_by_acquisition(
|
|||||||
return first.get_event_id() < second.get_event_id()
|
return first.get_event_id() < second.get_event_id()
|
||||||
|
|
||||||
|
|
||||||
static func _sort_communicable(first: Dictionary, second: Dictionary) -> bool:
|
static func _sort_by_importance(first: Dictionary, second: Dictionary) -> bool:
|
||||||
if bool(first["lasting"]) != bool(second["lasting"]):
|
if bool(first["lasting"]) != bool(second["lasting"]):
|
||||||
return bool(first["lasting"])
|
return bool(first["lasting"])
|
||||||
if int(first["acquired_tick"]) != int(second["acquired_tick"]):
|
if int(first["acquired_tick"]) != int(second["acquired_tick"]):
|
||||||
|
|||||||
@@ -59,18 +59,38 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Runtime knowledge UI check should use a completed simulation transaction"
|
"Runtime knowledge UI check should use a completed simulation transaction"
|
||||||
)
|
)
|
||||||
|
var world_view := main_scene.get_node("WorldViewManager")
|
||||||
|
var contributor_visual: Node3D = world_view.active_npc_visuals[contributor.id]
|
||||||
|
var witness_visual: Node3D = world_view.active_npc_visuals[witness.id]
|
||||||
|
var listener_visual: Node3D = world_view.active_npc_visuals[listener.id]
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
witness_visual.get_node("TrustReactionRoot").visible
|
||||||
|
and not contributor_visual.get_node("TrustReactionRoot").visible
|
||||||
|
),
|
||||||
|
"A real trust consequence should blossom only above the reacting observer"
|
||||||
|
)
|
||||||
|
var reaction_checksum: String = simulation_manager.get_state_checksum()
|
||||||
|
witness_visual.play_trust_gain_reaction()
|
||||||
|
_check(
|
||||||
|
simulation_manager.get_state_checksum() == reaction_checksum,
|
||||||
|
"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")
|
||||||
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(
|
||||||
(
|
(
|
||||||
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
|
"Memories 1 lasting · 0 recent" in inspector_label.text
|
||||||
and "witnessed · lasting" in inspector_label.text
|
and (
|
||||||
and "Memory: 1 lasting · 0 recent" in inspector_label.text
|
"◆ lasting · %s stocked 1 food · witnessed" % contributor.npc_name
|
||||||
|
in inspector_label.text
|
||||||
|
)
|
||||||
and "Relationship: %s" % contributor.npc_name in inspector_label.text
|
and "Relationship: %s" % contributor.npc_name in inspector_label.text
|
||||||
and "Because:" in inspector_label.text
|
and "Because:" in inspector_label.text
|
||||||
|
and "Own history" in inspector_label.text
|
||||||
),
|
),
|
||||||
"NPC inspector should show the witnessed fact and its relationship consequence"
|
"NPC history should show the lasting witnessed fact and its relationship consequence"
|
||||||
)
|
)
|
||||||
var guard_site := (
|
var guard_site := (
|
||||||
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
|
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
|
||||||
@@ -87,11 +107,17 @@ func _run() -> void:
|
|||||||
village_ui.call("_refresh_npc_inspector")
|
village_ui.call("_refresh_npc_inspector")
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
|
"Memories 0 lasting · 1 recent" in inspector_label.text
|
||||||
and "heard from %s · recent" % witness.npc_name in inspector_label.text
|
and (
|
||||||
and "Memory: 0 lasting · 1 recent" in inspector_label.text
|
"• recent · %s stocked 1 food · heard %s" % [contributor.npc_name, witness.npc_name]
|
||||||
|
in inspector_label.text
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"NPC inspector should identify who communicated a known fact"
|
"NPC history should identify who communicated a retained fact"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
not listener_visual.get_node("TrustReactionRoot").visible,
|
||||||
|
"Learning a fact without a relationship consequence should not show a trust blossom"
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
||||||
|
|||||||
@@ -84,6 +84,17 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Memory summaries should distinguish lasting and recent knowledge"
|
"Memory summaries should distinguish lasting and recent knowledge"
|
||||||
)
|
)
|
||||||
|
var lasting_history: Array = manager.get_retained_memory_events(lasting_listener.id, 4)
|
||||||
|
var recent_history: Array = manager.get_retained_memory_events(recent_listener.id, 4)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
lasting_history.size() == 1
|
||||||
|
and lasting_history[0] == deposit_event
|
||||||
|
and recent_history.size() == 1
|
||||||
|
and recent_history[0] == deposit_event
|
||||||
|
),
|
||||||
|
"Person-history queries should resolve each retained fact to objective history"
|
||||||
|
)
|
||||||
|
|
||||||
var objective_event_count: int = manager.economic_events.size()
|
var objective_event_count: int = manager.economic_events.size()
|
||||||
var objective_next_event_id: int = manager.next_event_id
|
var objective_next_event_id: int = manager.next_event_id
|
||||||
@@ -148,6 +159,13 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Forgetting knowledge must not prune or replay objective event history"
|
"Forgetting knowledge must not prune or replay objective event history"
|
||||||
)
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.get_retained_memory_events(recent_listener.id, 4).is_empty()
|
||||||
|
and deposit_event in manager.get_npc_events(contributor.id, 3)
|
||||||
|
),
|
||||||
|
"Forgotten subjective facts should leave the contributor's objective personal history intact"
|
||||||
|
)
|
||||||
var lasting_relationship: RelationshipStateRecord = (
|
var lasting_relationship: RelationshipStateRecord = (
|
||||||
manager.relationship_system.get_relationship(lasting_listener.id, contributor.id)
|
manager.relationship_system.get_relationship(lasting_listener.id, contributor.id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ func _run() -> void:
|
|||||||
visual.set_physics_process(false)
|
visual.set_physics_process(false)
|
||||||
var profession_definition := SimulationDefinitions.get_profession(npc.profession)
|
var profession_definition := SimulationDefinitions.get_profession(npc.profession)
|
||||||
var body: MeshInstance3D = visual.get_node("MeshInstance3D")
|
var body: MeshInstance3D = visual.get_node("MeshInstance3D")
|
||||||
|
var trust_reaction_root: Node3D = visual.get_node("TrustReactionRoot")
|
||||||
|
_check(not trust_reaction_root.visible, "A newly spawned visual should not replay reactions")
|
||||||
_check(
|
_check(
|
||||||
body.material_override.albedo_color == profession_definition.visual_color,
|
body.material_override.albedo_color == profession_definition.visual_color,
|
||||||
"NPC body color should come from its profession definition"
|
"NPC body color should come from its profession definition"
|
||||||
@@ -56,12 +58,56 @@ func _run() -> void:
|
|||||||
not visual.get_node("ProfessionLabel").visible and visual.get_node("TaskGlyphRoot").visible,
|
not visual.get_node("ProfessionLabel").visible and visual.get_node("TaskGlyphRoot").visible,
|
||||||
"Cinematic mode should hide debug labels while preserving task glyphs"
|
"Cinematic mode should hide debug labels while preserving task glyphs"
|
||||||
)
|
)
|
||||||
|
visual.play_trust_gain_reaction()
|
||||||
|
_check(trust_reaction_root.visible, "A direct trust reaction should reveal the blossom")
|
||||||
|
visual.play_trust_gain_reaction()
|
||||||
|
_check(
|
||||||
|
trust_reaction_root.visible and trust_reaction_root.get_child_count() == 4,
|
||||||
|
"Restarting a trust reaction should reuse one blossom without stacking visuals"
|
||||||
|
)
|
||||||
|
visual.set_debug_overlay_visible(false)
|
||||||
|
_check(
|
||||||
|
trust_reaction_root.visible,
|
||||||
|
"The trust reaction should remain visible in cinematic presentation mode"
|
||||||
|
)
|
||||||
|
var active_reaction_tween: Tween = visual.get("trust_reaction_tween")
|
||||||
|
await active_reaction_tween.finished
|
||||||
|
_check(
|
||||||
|
not trust_reaction_root.visible,
|
||||||
|
"The trust reaction should fade away instead of becoming permanent world clutter"
|
||||||
|
)
|
||||||
visual.set_debug_overlay_visible(true)
|
visual.set_debug_overlay_visible(true)
|
||||||
visual.set_task_presentation(SimulationIds.ACTION_WANDER, SimNPC.TASK_STATE_TRAVELING)
|
visual.set_task_presentation(SimulationIds.ACTION_WANDER, SimNPC.TASK_STATE_TRAVELING)
|
||||||
_check(
|
_check(
|
||||||
not visual.get_node("TaskGlyphRoot").visible,
|
not visual.get_node("TaskGlyphRoot").visible,
|
||||||
"Ambient wander should not display a task glyph"
|
"Ambient wander should not display a task glyph"
|
||||||
)
|
)
|
||||||
|
var routed_observer: SimNPC = manager.npcs[1]
|
||||||
|
var routed_subject: SimNPC = manager.npcs[2]
|
||||||
|
var observer_visual: Node3D = view.active_npc_visuals[routed_observer.id]
|
||||||
|
var subject_visual: Node3D = view.active_npc_visuals[routed_subject.id]
|
||||||
|
var routed_relationship := RelationshipStateRecord.create(
|
||||||
|
routed_observer.id, routed_subject.id, 0.5, 0.65, 9001
|
||||||
|
)
|
||||||
|
var routed_event := EconomicEventRecord.create(
|
||||||
|
9001,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
manager.tick_count,
|
||||||
|
routed_subject.id,
|
||||||
|
&"test_source",
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
1.0
|
||||||
|
)
|
||||||
|
manager.emit_signal("relationship_changed", routed_relationship, routed_event)
|
||||||
|
_check(
|
||||||
|
observer_visual.get_node("TrustReactionRoot").visible,
|
||||||
|
"The authoritative relationship signal should route a reaction to its observer"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
not subject_visual.get_node("TrustReactionRoot").visible,
|
||||||
|
"A routed trust reaction should not play on the relationship subject"
|
||||||
|
)
|
||||||
var test_decision := ActionSelectionResult.new(
|
var test_decision := ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_GATHER_FOOD,
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
-1.0,
|
-1.0,
|
||||||
@@ -176,6 +222,16 @@ func _run() -> void:
|
|||||||
visual_after_restore.global_position.is_equal_approx(npc.position),
|
visual_after_restore.global_position.is_equal_approx(npc.position),
|
||||||
"Rebuilt presentation should use the restored authoritative position"
|
"Rebuilt presentation should use the restored authoritative position"
|
||||||
)
|
)
|
||||||
|
_check(
|
||||||
|
not visual_after_restore.get_node("TrustReactionRoot").visible,
|
||||||
|
"Restoring state should rebuild the visual without replaying transient reactions"
|
||||||
|
)
|
||||||
|
visual_after_restore.play_trust_gain_reaction()
|
||||||
|
visual_after_restore.apply_dead_visual_state()
|
||||||
|
_check(
|
||||||
|
not visual_after_restore.get_node("TrustReactionRoot").visible,
|
||||||
|
"Death presentation should suppress an active trust reaction"
|
||||||
|
)
|
||||||
|
|
||||||
if failures.is_empty():
|
if failures.is_empty():
|
||||||
print("[TEST] NPC visual lifecycle passed")
|
print("[TEST] NPC visual lifecycle passed")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const SimulationEventLogScript := preload("res://simulation/events/SimulationEve
|
|||||||
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
||||||
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||||
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
||||||
|
const SimulationManagerScript := preload("res://simulation/SimulationManager.gd")
|
||||||
|
|
||||||
|
|
||||||
func test_storage_never_accepts_more_than_its_capacity() -> void:
|
func test_storage_never_accepts_more_than_its_capacity() -> void:
|
||||||
@@ -211,9 +212,48 @@ func test_knowledge_retention_is_bounded_stable_and_importance_ranked() -> void:
|
|||||||
|
|
||||||
var ranked_system := EventKnowledgeSystemScript.new()
|
var ranked_system := EventKnowledgeSystemScript.new()
|
||||||
var ranked_records: Array[KnownEventStateRecord] = [
|
var ranked_records: Array[KnownEventStateRecord] = [
|
||||||
KnownEventStateRecord.create(1, 1, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 10),
|
KnownEventStateRecord.create(1, 10, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 5),
|
||||||
KnownEventStateRecord.create(1, 2, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 20),
|
KnownEventStateRecord.create(1, 20, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 20),
|
||||||
|
KnownEventStateRecord.create(
|
||||||
|
1,
|
||||||
|
30,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
2,
|
||||||
|
20,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
),
|
||||||
|
KnownEventStateRecord.create(1, 40, SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY, -1, 10),
|
||||||
]
|
]
|
||||||
ranked_system.restore(ranked_records)
|
ranked_system.restore(ranked_records)
|
||||||
var lasting_event_ids: Array[int] = [1]
|
var lasting_event_ids: Array[int] = [10]
|
||||||
assert_eq(ranked_system.get_communicable_event_ids(1, lasting_event_ids), [1, 2])
|
assert_eq(ranked_system.get_retained_event_ids(1, lasting_event_ids, 0), [10, 30, 20, 40])
|
||||||
|
assert_eq(ranked_system.get_retained_event_ids(1, lasting_event_ids, 2), [10, 30])
|
||||||
|
assert_eq(ranked_system.get_communicable_event_ids(1, lasting_event_ids), [10, 20])
|
||||||
|
|
||||||
|
var objective_events: Array[EconomicEventRecord] = []
|
||||||
|
for event_id in [10, 20, 30, 40]:
|
||||||
|
objective_events.append(
|
||||||
|
EconomicEventRecord.create(
|
||||||
|
event_id,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SimulationIds.npc_inventory_id(0),
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
1.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var manager := SimulationManagerScript.new()
|
||||||
|
manager.event_log.restore(objective_events, 41)
|
||||||
|
manager.event_knowledge_system.restore(ranked_records)
|
||||||
|
var relationships: Array[RelationshipStateRecord] = [
|
||||||
|
RelationshipStateRecord.create(1, 0, 0.5, 0.65, 10)
|
||||||
|
]
|
||||||
|
manager.relationship_system.restore(relationships)
|
||||||
|
var retained_events: Array[EconomicEventRecord] = manager.get_retained_memory_events(1, 3)
|
||||||
|
var retained_ids: Array[int] = []
|
||||||
|
for event in retained_events:
|
||||||
|
retained_ids.append(int(event.data["event_id"]))
|
||||||
|
assert_eq(retained_ids, [10, 30, 20])
|
||||||
|
manager.free()
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ func _run() -> void:
|
|||||||
if not demo_controller.debug_overlay_visible:
|
if not demo_controller.debug_overlay_visible:
|
||||||
demo_controller.toggle_debug_overlay()
|
demo_controller.toggle_debug_overlay()
|
||||||
|
|
||||||
|
if not _stage_social_history(main_scene):
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
await _stage_wind_gust(main_scene)
|
await _stage_wind_gust(main_scene)
|
||||||
|
await _settle_trust_reaction()
|
||||||
var debug_analysis := _capture(DEBUG_OUTPUT_PATH)
|
var debug_analysis := _capture(DEBUG_OUTPUT_PATH)
|
||||||
if debug_analysis.is_empty():
|
if debug_analysis.is_empty():
|
||||||
quit(1)
|
quit(1)
|
||||||
@@ -49,7 +53,11 @@ func _run() -> void:
|
|||||||
|
|
||||||
if demo_controller.has_method("toggle_debug_overlay") and demo_controller.debug_overlay_visible:
|
if demo_controller.has_method("toggle_debug_overlay") and demo_controller.debug_overlay_visible:
|
||||||
demo_controller.toggle_debug_overlay()
|
demo_controller.toggle_debug_overlay()
|
||||||
|
if not _restart_trust_reaction(main_scene):
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
await _stage_wind_gust(main_scene)
|
await _stage_wind_gust(main_scene)
|
||||||
|
await _settle_trust_reaction()
|
||||||
|
|
||||||
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
|
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
|
||||||
if cinematic_analysis.is_empty():
|
if cinematic_analysis.is_empty():
|
||||||
@@ -65,6 +73,83 @@ func _run() -> void:
|
|||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _stage_social_history(main_scene: Node) -> bool:
|
||||||
|
var simulation_manager := main_scene.get_node("SimulationManager")
|
||||||
|
var world_view := main_scene.get_node("WorldViewManager")
|
||||||
|
if simulation_manager.npcs.size() < 2:
|
||||||
|
push_error("Simulation Garden social-history staging needs two NPCs")
|
||||||
|
return false
|
||||||
|
var contributor: SimNPC = simulation_manager.npcs[0]
|
||||||
|
var observer: SimNPC = simulation_manager.npcs[1]
|
||||||
|
var contributor_visual: Node3D = world_view.active_npc_visuals.get(contributor.id)
|
||||||
|
var observer_visual: Node3D = world_view.active_npc_visuals.get(observer.id)
|
||||||
|
if contributor_visual == null or observer_visual == null:
|
||||||
|
push_error("Simulation Garden social-history staging needs both active NPC visuals")
|
||||||
|
return false
|
||||||
|
var relationship: RelationshipStateRecord = (
|
||||||
|
simulation_manager.relationship_system.get_relationship(observer.id, contributor.id)
|
||||||
|
)
|
||||||
|
if relationship == null:
|
||||||
|
push_error("Simulation Garden staging pair needs an existing directed relationship")
|
||||||
|
return false
|
||||||
|
var previous_trust := relationship.get_trust()
|
||||||
|
simulation_manager.set_process(false)
|
||||||
|
contributor_visual.set_physics_process(false)
|
||||||
|
observer_visual.set_physics_process(false)
|
||||||
|
observer_visual.global_position = contributor_visual.global_position + Vector3(1.0, 0.0, 0.0)
|
||||||
|
simulation_manager.synchronize_npc_position(contributor.id, contributor_visual.global_position)
|
||||||
|
simulation_manager.synchronize_npc_position(observer.id, observer_visual.global_position)
|
||||||
|
observer.hunger = 85.0
|
||||||
|
contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
|
var deposited: float = simulation_manager.economy.deposit_inventory(
|
||||||
|
contributor, SimulationIds.RESOURCE_FOOD
|
||||||
|
)
|
||||||
|
if not is_equal_approx(deposited, 1.0):
|
||||||
|
push_error("Simulation Garden staging could not deposit one food")
|
||||||
|
return false
|
||||||
|
simulation_manager.emit_signal("village_changed", simulation_manager.village)
|
||||||
|
var contributor_events: Array = simulation_manager.get_npc_events(contributor.id, 1)
|
||||||
|
if contributor_events.is_empty():
|
||||||
|
push_error("Simulation Garden staging did not record the deposit event")
|
||||||
|
return false
|
||||||
|
var deposit_event := contributor_events[0] as EconomicEventRecord
|
||||||
|
var retained_memories: Array = simulation_manager.get_retained_memory_events(observer.id, 1)
|
||||||
|
var reaction_root := observer_visual.get_node_or_null("TrustReactionRoot") as Node3D
|
||||||
|
if (
|
||||||
|
StringName(deposit_event.data["event_type"]) != SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||||
|
or relationship.get_trust() <= previous_trust
|
||||||
|
or relationship.get_last_trust_cause_event_id() != int(deposit_event.data["event_id"])
|
||||||
|
or retained_memories.is_empty()
|
||||||
|
or retained_memories[0] != deposit_event
|
||||||
|
or not simulation_manager.is_known_event_lasting(
|
||||||
|
observer.id, int(deposit_event.data["event_id"])
|
||||||
|
)
|
||||||
|
or reaction_root == null
|
||||||
|
or not reaction_root.visible
|
||||||
|
):
|
||||||
|
push_error("Simulation Garden staging did not produce the retained trust consequence")
|
||||||
|
return false
|
||||||
|
var village_ui := main_scene.get_node("UI")
|
||||||
|
village_ui.selected_npc_index = simulation_manager.npcs.find(observer)
|
||||||
|
village_ui.call("_refresh_npc_inspector")
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _restart_trust_reaction(main_scene: Node) -> bool:
|
||||||
|
var simulation_manager := main_scene.get_node("SimulationManager")
|
||||||
|
var world_view := main_scene.get_node("WorldViewManager")
|
||||||
|
if simulation_manager.npcs.size() < 2:
|
||||||
|
push_error("Simulation Garden trust-reaction staging needs two NPCs")
|
||||||
|
return false
|
||||||
|
var observer: SimNPC = simulation_manager.npcs[1]
|
||||||
|
var observer_visual: Node3D = world_view.active_npc_visuals.get(observer.id)
|
||||||
|
if observer_visual == null or not observer_visual.has_method("play_trust_gain_reaction"):
|
||||||
|
push_error("Simulation Garden trust-reaction staging needs an active observer visual")
|
||||||
|
return false
|
||||||
|
observer_visual.play_trust_gain_reaction()
|
||||||
|
return observer_visual.get_node("TrustReactionRoot").visible
|
||||||
|
|
||||||
|
|
||||||
func _stage_wind_gust(main_scene: Node) -> void:
|
func _stage_wind_gust(main_scene: Node) -> void:
|
||||||
var gust_field := (
|
var gust_field := (
|
||||||
main_scene.get_node_or_null("JajceWorld/AtmosphereRoot/WindGustField") as WindGustField
|
main_scene.get_node_or_null("JajceWorld/AtmosphereRoot/WindGustField") as WindGustField
|
||||||
@@ -76,6 +161,11 @@ func _stage_wind_gust(main_scene: Node) -> void:
|
|||||||
await process_frame
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _settle_trust_reaction() -> void:
|
||||||
|
for _frame in 12:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
func _capture(output_path: String) -> Dictionary:
|
func _capture(output_path: String) -> Dictionary:
|
||||||
var image := root.get_texture().get_image()
|
var image := root.get_texture().get_image()
|
||||||
if image == null or image.is_empty():
|
if image == null or image.is_empty():
|
||||||
|
|||||||
+56
-42
@@ -188,8 +188,7 @@ func _refresh_npc_inspector() -> void:
|
|||||||
score_text += "\n\nUnavailable\n" + "\n".join(rejection_rows)
|
score_text += "\n\nUnavailable\n" + "\n".join(rejection_rows)
|
||||||
var event_text := _build_event_history(npc)
|
var event_text := _build_event_history(npc)
|
||||||
var relationship_text := _build_relationship_display(npc)
|
var relationship_text := _build_relationship_display(npc)
|
||||||
var memory_text := _build_memory_display(npc)
|
var memory_history_text := _build_memory_history_display(npc)
|
||||||
var known_fact_text := _build_known_fact_display(npc)
|
|
||||||
npc_inspector_label.text = (
|
npc_inspector_label.text = (
|
||||||
(
|
(
|
||||||
"%s · %s\n"
|
"%s · %s\n"
|
||||||
@@ -199,9 +198,8 @@ func _refresh_npc_inspector() -> void:
|
|||||||
+ "Carrying food: %.0f wood: %.0f\n"
|
+ "Carrying food: %.0f wood: %.0f\n"
|
||||||
+ "%s\n"
|
+ "%s\n"
|
||||||
+ "%s\n"
|
+ "%s\n"
|
||||||
+ "%s\n"
|
|
||||||
+ "Why\n%s%s\n\n"
|
+ "Why\n%s%s\n\n"
|
||||||
+ "Recent\n%s\n\n"
|
+ "Own history\n%s\n\n"
|
||||||
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
||||||
)
|
)
|
||||||
% [
|
% [
|
||||||
@@ -215,8 +213,7 @@ func _refresh_npc_inspector() -> void:
|
|||||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||||
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
||||||
relationship_text,
|
relationship_text,
|
||||||
memory_text,
|
memory_history_text,
|
||||||
known_fact_text,
|
|
||||||
reason_text,
|
reason_text,
|
||||||
score_text,
|
score_text,
|
||||||
event_text
|
event_text
|
||||||
@@ -245,7 +242,7 @@ func set_debug_overlay_visible(is_visible: bool) -> void:
|
|||||||
func _build_event_history(npc: SimNPC) -> String:
|
func _build_event_history(npc: SimNPC) -> String:
|
||||||
if not simulation_manager.has_method("get_npc_events"):
|
if not simulation_manager.has_method("get_npc_events"):
|
||||||
return ""
|
return ""
|
||||||
var events: Array = simulation_manager.get_npc_events(npc.id, 5)
|
var events: Array = simulation_manager.get_npc_events(npc.id, 3)
|
||||||
if events.is_empty():
|
if events.is_empty():
|
||||||
return "No recorded events yet"
|
return "No recorded events yet"
|
||||||
var name_map := {}
|
var name_map := {}
|
||||||
@@ -287,48 +284,65 @@ func _build_relationship_display(npc: SimNPC) -> String:
|
|||||||
return "%s\nBecause: %s" % [display, cause.description(name_map)]
|
return "%s\nBecause: %s" % [display, cause.description(name_map)]
|
||||||
|
|
||||||
|
|
||||||
func _build_known_fact_display(npc: SimNPC) -> String:
|
func _build_memory_history_display(npc: SimNPC) -> String:
|
||||||
if not simulation_manager.has_method("get_known_events"):
|
if not simulation_manager.has_method("get_retained_memory_events"):
|
||||||
return ""
|
return "Memories unavailable"
|
||||||
var known_events: Array = simulation_manager.get_known_events(npc.id, 1)
|
var summary: Dictionary = simulation_manager.get_memory_summary(npc.id)
|
||||||
if known_events.is_empty():
|
var header := (
|
||||||
return "Known fact: none yet"
|
"Memories %d lasting · %d recent"
|
||||||
|
% [int(summary.get("lasting", 0)), int(summary.get("recent", 0))]
|
||||||
|
)
|
||||||
|
var retained_events: Array = simulation_manager.get_retained_memory_events(npc.id, 4)
|
||||||
|
if retained_events.is_empty():
|
||||||
|
return header + "\nNo retained memories"
|
||||||
var name_map := {}
|
var name_map := {}
|
||||||
for other_npc in simulation_manager.npcs:
|
for other_npc in simulation_manager.npcs:
|
||||||
name_map[other_npc.id] = other_npc.npc_name
|
name_map[other_npc.id] = other_npc.npc_name
|
||||||
var latest_event := known_events[known_events.size() - 1] as EconomicEventRecord
|
var lines: Array[String] = [header]
|
||||||
var provenance := "source not recorded"
|
for event in retained_events:
|
||||||
var retention := "recent"
|
var event_id := int(event.data["event_id"])
|
||||||
if simulation_manager.has_method("get_known_event_record"):
|
|
||||||
var known_record: KnownEventStateRecord = simulation_manager.get_known_event_record(
|
var known_record: KnownEventStateRecord = simulation_manager.get_known_event_record(
|
||||||
npc.id, int(latest_event.data["event_id"])
|
npc.id, event_id
|
||||||
)
|
)
|
||||||
if known_record != null:
|
if known_record == null:
|
||||||
match known_record.get_acquisition_method():
|
continue
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
var lasting: bool = simulation_manager.is_known_event_lasting(npc.id, event_id)
|
||||||
provenance = "performed personally"
|
var marker := "◆" if lasting else "•"
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
var retention := "lasting" if lasting else "recent"
|
||||||
provenance = "witnessed"
|
(
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
lines
|
||||||
var source_name: String = name_map.get(
|
. append(
|
||||||
known_record.get_source_npc_id(), "someone"
|
(
|
||||||
|
"%s %s · %s · %s"
|
||||||
|
% [
|
||||||
|
marker,
|
||||||
|
retention,
|
||||||
|
_build_compact_memory_summary(event, name_map),
|
||||||
|
_build_memory_provenance(known_record, name_map),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
provenance = "heard from %s" % source_name
|
)
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
)
|
||||||
provenance = "source not recorded"
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_compact_memory_summary(event: EconomicEventRecord, name_map: Dictionary) -> String:
|
||||||
if (
|
if (
|
||||||
simulation_manager.has_method("is_known_event_lasting")
|
StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||||
and (simulation_manager.is_known_event_lasting(npc.id, int(latest_event.data["event_id"])))
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||||
):
|
):
|
||||||
retention = "lasting"
|
var actor_name: String = name_map.get(int(event.data["actor_id"]), "Someone")
|
||||||
return "Known fact: %s (%s · %s)" % [latest_event.description(name_map), provenance, retention]
|
return "%s stocked %.0f food" % [actor_name, float(event.data["amount"])]
|
||||||
|
return event.description(name_map)
|
||||||
|
|
||||||
|
|
||||||
func _build_memory_display(npc: SimNPC) -> String:
|
func _build_memory_provenance(record: KnownEventStateRecord, name_map: Dictionary) -> String:
|
||||||
if not simulation_manager.has_method("get_memory_summary"):
|
match record.get_acquisition_method():
|
||||||
return ""
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
||||||
var summary: Dictionary = simulation_manager.get_memory_summary(npc.id)
|
return "own action"
|
||||||
return (
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
||||||
"Memory: %d lasting · %d recent"
|
return "witnessed"
|
||||||
% [int(summary.get("lasting", 0)), int(summary.get("recent", 0))]
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
)
|
var source_name: String = name_map.get(record.get_source_npc_id(), "someone")
|
||||||
|
return "heard %s" % source_name
|
||||||
|
return "source unknown"
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ func initialize_world_view() -> void:
|
|||||||
simulation_manager.npc_task_changed.connect(_on_npc_task_changed)
|
simulation_manager.npc_task_changed.connect(_on_npc_task_changed)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no npc_task_changed signal")
|
push_error("WorldViewManager: SimulationManager has no npc_task_changed signal")
|
||||||
|
if simulation_manager.has_signal("relationship_changed"):
|
||||||
|
simulation_manager.relationship_changed.connect(_on_relationship_changed)
|
||||||
|
else:
|
||||||
|
push_error("WorldViewManager: SimulationManager has no relationship_changed signal")
|
||||||
if simulation_manager.has_signal("state_restored"):
|
if simulation_manager.has_signal("state_restored"):
|
||||||
simulation_manager.state_restored.connect(_on_simulation_state_restored)
|
simulation_manager.state_restored.connect(_on_simulation_state_restored)
|
||||||
else:
|
else:
|
||||||
@@ -182,6 +186,20 @@ func _on_npc_task_changed(npc: SimNPC, _old_task: StringName, _new_task: StringN
|
|||||||
push_error("WorldViewManager: NPCVisual has no set_task_presentation method")
|
push_error("WorldViewManager: NPCVisual has no set_task_presentation method")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_relationship_changed(
|
||||||
|
relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord
|
||||||
|
) -> void:
|
||||||
|
if relationship == null:
|
||||||
|
return
|
||||||
|
var visual = active_npc_visuals.get(relationship.get_observer_id())
|
||||||
|
if visual == null:
|
||||||
|
return
|
||||||
|
if visual.has_method("play_trust_gain_reaction"):
|
||||||
|
visual.play_trust_gain_reaction()
|
||||||
|
else:
|
||||||
|
push_error("WorldViewManager: NPCVisual has no play_trust_gain_reaction method")
|
||||||
|
|
||||||
|
|
||||||
func _on_simulation_state_restored() -> void:
|
func _on_simulation_state_restored() -> void:
|
||||||
for visual in active_npc_visuals.values():
|
for visual in active_npc_visuals.values():
|
||||||
visual.queue_free()
|
visual.queue_free()
|
||||||
|
|||||||
Reference in New Issue
Block a user