From 4b5fc858f817c2c461fa6d058403c67f7dee800b Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Thu, 9 Jul 2026 00:23:00 +0200 Subject: [PATCH] feat: NPC mourning behavior on housemate death When an NPC dies from starvation, their highest-familiarity companion enters mourning for 100 ticks (~half a day). Mourning NPCs skip work: they rest if energy is low, otherwise wander near home. Mourning_ticks counts down each tick through ActionExecutionSystem and is serialized for save/load continuity with backward-compatible default of 0. This gives the familiarity graph its first behavioral consequence. --- simulation/SimNPC.gd | 1 + simulation/SimulationManager.gd | 25 +++++++++++++++++++++ simulation/actions/ActionExecutionSystem.gd | 2 ++ simulation/actions/ActionSelectionSystem.gd | 8 +++++++ simulation/state/NPCStateRecord.gd | 4 +++- 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/simulation/SimNPC.gd b/simulation/SimNPC.gd index 4e770ff..771634f 100644 --- a/simulation/SimNPC.gd +++ b/simulation/SimNPC.gd @@ -33,6 +33,7 @@ var inventory: Dictionary = {} var last_task: StringName var random_source: RandomNumberGenerator var familiarity: Dictionary = {} +var mourning_ticks := 0 var debug_logs := true diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index 1c341ab..a560702 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -149,6 +149,7 @@ func simulate_tick() -> void: npc_died.emit(npc) npc_task_changed.emit(npc, old_task, npc.current_task) record_narrative_event(SimulationIds.EVENT_NPC_DIED, npc.id) + _notify_mourning(npc) if debug_logs: print("[SimulationManager] NPC died: ", npc.npc_name) @@ -491,6 +492,30 @@ func _npc_inventory_id(npc_id: int) -> StringName: return StringName("npc_%d_inventory" % npc_id) +func _notify_mourning(dead_npc: SimNPC) -> void: + var best_id := -1 + var best_score := -1.0 + for key in dead_npc.familiarity: + var other_id: int = int(key) + var score: float = float(dead_npc.familiarity[key]) + if score > best_score: + best_score = score + best_id = other_id + if best_id < 0: + return + for npc in npcs: + if npc.id == best_id and not npc.is_dead: + npc.mourning_ticks = 100 + if debug_logs: + print( + "[SimulationManager] ", + npc.npc_name, + " is mourning ", + dead_npc.npc_name + ) + return + + func _record_economic_event( event_type: StringName, actor_id: int, diff --git a/simulation/actions/ActionExecutionSystem.gd b/simulation/actions/ActionExecutionSystem.gd index 6439b2d..fdd5bdf 100644 --- a/simulation/actions/ActionExecutionSystem.gd +++ b/simulation/actions/ActionExecutionSystem.gd @@ -6,6 +6,8 @@ func advance_npc(npc: SimNPC, village: SimVillage) -> void: if npc.is_dead: return _update_needs(npc, village) + if npc.mourning_ticks > 0: + npc.mourning_ticks -= 1 if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_WORKING: return npc.task_progress += 1.0 diff --git a/simulation/actions/ActionSelectionSystem.gd b/simulation/actions/ActionSelectionSystem.gd index 2dedd79..aea75c0 100644 --- a/simulation/actions/ActionSelectionSystem.gd +++ b/simulation/actions/ActionSelectionSystem.gd @@ -34,6 +34,14 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, a return ActionSelectionResult.new( SimulationIds.ACTION_GATHER_FOOD, -1.0, "Starving; must find food" ) + if npc.mourning_ticks > 0: + if npc.energy < 40.0: + return ActionSelectionResult.new( + SimulationIds.ACTION_REST, -1.0, "Mourning; seeking rest" + ) + return ActionSelectionResult.new( + SimulationIds.ACTION_WANDER, -1.0, "Mourning; wandering near home" + ) if npc.hunger > 80.0: if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0: return ActionSelectionResult.new( diff --git a/simulation/state/NPCStateRecord.gd b/simulation/state/NPCStateRecord.gd index 4a4c78d..ca393d9 100644 --- a/simulation/state/NPCStateRecord.gd +++ b/simulation/state/NPCStateRecord.gd @@ -46,7 +46,8 @@ static func capture(npc: SimNPC) -> NPCStateRecord: "last_task": String(npc.last_task), "random_seed": str(npc.random_source.seed), "random_state": str(npc.random_source.state), - "familiarity": _sorted_familiarity(npc.familiarity) + "familiarity": _sorted_familiarity(npc.familiarity), + "mourning_ticks": npc.mourning_ticks } ) @@ -168,6 +169,7 @@ func restore(debug_logs: bool) -> SimNPC: npc.familiarity[int(pair_dict["id"])] = float(pair_dict["score"]) elif saved_familiarity is Dictionary: npc.familiarity = saved_familiarity.duplicate(true) + npc.mourning_ticks = int(data.get("mourning_ticks", 0)) return npc