diff --git a/simulation/SimNPC.gd b/simulation/SimNPC.gd index 39aa8aa..4e770ff 100644 --- a/simulation/SimNPC.gd +++ b/simulation/SimNPC.gd @@ -32,6 +32,7 @@ var has_travel_target := false var inventory: Dictionary = {} var last_task: StringName var random_source: RandomNumberGenerator +var familiarity: Dictionary = {} var debug_logs := true diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index 1416c7f..aa64c07 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -86,6 +86,12 @@ func generate_npcs() -> void: for i in range(npcs.size()): npcs[i].home_position = home_positions[i % home_count] + for i in range(npcs.size()): + for j in range(i + 1, npcs.size()): + if npcs[i].home_position.distance_to(npcs[j].home_position) < 4.0: + npcs[i].familiarity[npcs[j].id] = 0.5 + npcs[j].familiarity[npcs[i].id] = 0.5 + func _create_random_source(npc_id: int, stream_id: int) -> RandomNumberGenerator: var source := RandomNumberGenerator.new() diff --git a/simulation/state/NPCStateRecord.gd b/simulation/state/NPCStateRecord.gd index e19c44c..e4ed081 100644 --- a/simulation/state/NPCStateRecord.gd +++ b/simulation/state/NPCStateRecord.gd @@ -45,7 +45,8 @@ static func capture(npc: SimNPC) -> NPCStateRecord: "inventory": npc.inventory.duplicate(true), "last_task": String(npc.last_task), "random_seed": str(npc.random_source.seed), - "random_state": str(npc.random_source.state) + "random_state": str(npc.random_source.state), + "familiarity": npc.familiarity.duplicate(true) } ) @@ -160,6 +161,9 @@ func restore(debug_logs: bool) -> SimNPC: npc.last_task = StringName(data["last_task"]) npc.random_source.state = String(data["random_state"]).to_int() npc.debug_logs = debug_logs + var saved_familiarity = data.get("familiarity", {}) + if saved_familiarity is Dictionary: + npc.familiarity = saved_familiarity.duplicate(true) return npc diff --git a/tests/npc_schedule_test.gd b/tests/npc_schedule_test.gd index 9a9d271..1ab8f80 100644 --- a/tests/npc_schedule_test.gd +++ b/tests/npc_schedule_test.gd @@ -14,6 +14,7 @@ func _run() -> void: _test_sleep_restores_energy() _test_schedule_serialization() _test_sleep_targets_home() + _test_household_familiarity() if failures.is_empty(): print("[TEST] NPC schedule passed") @@ -181,6 +182,63 @@ func _test_sleep_targets_home() -> void: manager.free() +func _test_household_familiarity() -> void: + var manager: Node = load("res://simulation/SimulationManager.gd").new() + manager.simulation_seed = 700 + manager.debug_logs = false + manager.set_process(false) + manager.home_positions = [ + Vector3(-15.0, 2.0, 7.0), + Vector3(-15.0, 2.0, 8.0), + Vector3(-8.5, 2.0, 3.0), + Vector3(-6.5, 2.0, 1.5), + Vector3(20.0, 0.0, 20.0), + Vector3(22.0, 0.0, 22.0) + ] + root.add_child(manager) + + var a: SimNPC = manager.npcs[0] + var b: SimNPC = manager.npcs[1] + var c: SimNPC = manager.npcs[2] + var d: SimNPC = manager.npcs[3] + var e: SimNPC = manager.npcs[4] + + _check( + a.familiarity.has(b.id) and b.familiarity.has(a.id), + "NPCs with nearby homes should be mutual household members" + ) + _check( + is_equal_approx(float(a.familiarity[b.id]), 0.5), + "Household familiarity should start at baseline 0.5" + ) + _check( + c.familiarity.has(d.id) and d.familiarity.has(c.id), + "Second household pair should also be familiar" + ) + _check( + not a.familiarity.has(e.id), + "Distant NPCs should not have household familiarity" + ) + + var saved_json: String = manager.serialize_state() + var restored: Node = load("res://simulation/SimulationManager.gd").new() + restored.debug_logs = false + root.add_child(restored) + restored.set_process(false) + _check( + restored.restore_state_from_json(saved_json), + "Familiarity should survive save/load round-trip" + ) + var ra: SimNPC = restored.npcs[0] + _check( + ra.familiarity.size() > 0, + "Restored NPC should retain household familiarity" + ) + + manager.free() + restored.free() + + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message)