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.
This commit is contained in:
@@ -33,6 +33,7 @@ var inventory: Dictionary = {}
|
|||||||
var last_task: StringName
|
var last_task: StringName
|
||||||
var random_source: RandomNumberGenerator
|
var random_source: RandomNumberGenerator
|
||||||
var familiarity: Dictionary = {}
|
var familiarity: Dictionary = {}
|
||||||
|
var mourning_ticks := 0
|
||||||
var debug_logs := true
|
var debug_logs := true
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ func simulate_tick() -> void:
|
|||||||
npc_died.emit(npc)
|
npc_died.emit(npc)
|
||||||
npc_task_changed.emit(npc, old_task, npc.current_task)
|
npc_task_changed.emit(npc, old_task, npc.current_task)
|
||||||
record_narrative_event(SimulationIds.EVENT_NPC_DIED, npc.id)
|
record_narrative_event(SimulationIds.EVENT_NPC_DIED, npc.id)
|
||||||
|
_notify_mourning(npc)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("[SimulationManager] NPC died: ", npc.npc_name)
|
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)
|
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(
|
func _record_economic_event(
|
||||||
event_type: StringName,
|
event_type: StringName,
|
||||||
actor_id: int,
|
actor_id: int,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ func advance_npc(npc: SimNPC, village: SimVillage) -> void:
|
|||||||
if npc.is_dead:
|
if npc.is_dead:
|
||||||
return
|
return
|
||||||
_update_needs(npc, village)
|
_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:
|
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_WORKING:
|
||||||
return
|
return
|
||||||
npc.task_progress += 1.0
|
npc.task_progress += 1.0
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, a
|
|||||||
return ActionSelectionResult.new(
|
return ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Starving; must find food"
|
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.hunger > 80.0:
|
||||||
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
||||||
return ActionSelectionResult.new(
|
return ActionSelectionResult.new(
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
|
|||||||
"last_task": String(npc.last_task),
|
"last_task": String(npc.last_task),
|
||||||
"random_seed": str(npc.random_source.seed),
|
"random_seed": str(npc.random_source.seed),
|
||||||
"random_state": str(npc.random_source.state),
|
"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"])
|
npc.familiarity[int(pair_dict["id"])] = float(pair_dict["score"])
|
||||||
elif saved_familiarity is Dictionary:
|
elif saved_familiarity is Dictionary:
|
||||||
npc.familiarity = saved_familiarity.duplicate(true)
|
npc.familiarity = saved_familiarity.duplicate(true)
|
||||||
|
npc.mourning_ticks = int(data.get("mourning_ticks", 0))
|
||||||
return npc
|
return npc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user