feat: add person history and trust reactions

This commit is contained in:
Rijad Zuzo
2026-07-12 11:42:10 +02:00
parent 99b8146f75
commit 9fcf6a0a58
19 changed files with 543 additions and 90 deletions
+34 -8
View File
@@ -59,18 +59,38 @@ func _run() -> void:
),
"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")
village_ui.selected_npc_index = witness.id
village_ui.call("_refresh_npc_inspector")
_check(
(
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
and "witnessed · lasting" in inspector_label.text
and "Memory: 1 lasting · 0 recent" in inspector_label.text
"Memories 1 lasting · 0 recent" in inspector_label.text
and (
" lasting · %s stocked 1 food · witnessed" % contributor.npc_name
in inspector_label.text
)
and "Relationship: %s" % contributor.npc_name 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 := (
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
@@ -87,11 +107,17 @@ func _run() -> void:
village_ui.call("_refresh_npc_inspector")
_check(
(
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
and "heard from %s · recent" % witness.npc_name in inspector_label.text
and "Memory: 0 lasting · 1 recent" in inspector_label.text
"Memories 0 lasting · 1 recent" in inspector_label.text
and (
"• 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(
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
@@ -84,6 +84,17 @@ func _run() -> void:
),
"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_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"
)
_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 = (
manager.relationship_system.get_relationship(lasting_listener.id, contributor.id)
)
+56
View File
@@ -23,6 +23,8 @@ func _run() -> void:
visual.set_physics_process(false)
var profession_definition := SimulationDefinitions.get_profession(npc.profession)
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(
body.material_override.albedo_color == profession_definition.visual_color,
"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,
"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_task_presentation(SimulationIds.ACTION_WANDER, SimNPC.TASK_STATE_TRAVELING)
_check(
not visual.get_node("TaskGlyphRoot").visible,
"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(
SimulationIds.ACTION_GATHER_FOOD,
-1.0,
@@ -176,6 +222,16 @@ func _run() -> void:
visual_after_restore.global_position.is_equal_approx(npc.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():
print("[TEST] NPC visual lifecycle passed")
+44 -4
View File
@@ -4,6 +4,7 @@ const SimulationEventLogScript := preload("res://simulation/events/SimulationEve
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.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:
@@ -211,9 +212,48 @@ func test_knowledge_retention_is_bounded_stable_and_importance_ranked() -> void:
var ranked_system := EventKnowledgeSystemScript.new()
var ranked_records: Array[KnownEventStateRecord] = [
KnownEventStateRecord.create(1, 1, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 10),
KnownEventStateRecord.create(1, 2, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 20),
KnownEventStateRecord.create(1, 10, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 5),
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)
var lasting_event_ids: Array[int] = [1]
assert_eq(ranked_system.get_communicable_event_ids(1, lasting_event_ids), [1, 2])
var lasting_event_ids: Array[int] = [10]
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()