feat: runtime familiarity from shared work and utility exposure

NPCs who target the same resource or site concurrently gain mutual familiarity +0.1 per encounter, capped at 1.0. Action selection now receives the full NPC array and adds a +0.3 utility bonus for actions that familiar NPCs are currently performing - NPCs gravitate toward their housemates' professions. Inspector shows the highest-familiarity NPC with a percentage score under the resource lines.
This commit is contained in:
2026-07-09 00:09:50 +02:00
parent fce5c533fe
commit 595d67724f
3 changed files with 56 additions and 5 deletions
+18 -1
View File
@@ -127,7 +127,7 @@ func simulate_tick() -> void:
and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE] and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE] and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
): ):
var selection := action_selector.select_action(npc, village, clock.time_of_day()) var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs)
if selection != null: if selection != null:
latest_decisions[npc.id] = selection latest_decisions[npc.id] = selection
npc_decision_recorded.emit(npc, selection) npc_decision_recorded.emit(npc, selection)
@@ -360,6 +360,23 @@ func notify_npc_arrived(npc_id: int) -> void:
npc.has_travel_target = false npc.has_travel_target = false
npc.start_working() npc.start_working()
if npc.target_id != &"":
for other in npcs:
if other.id == npc.id or other.is_dead:
continue
if (
other.target_id == npc.target_id
and other.task_state in [
SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING
]
):
npc.familiarity[other.id] = minf(
float(npc.familiarity.get(other.id, 0.0)) + 0.1, 1.0
)
other.familiarity[npc.id] = minf(
float(other.familiarity.get(npc.id, 0.0)) + 0.1, 1.0
)
if debug_logs: if debug_logs:
print( print(
"[SimulationManager] ", "[SimulationManager] ",
+13 -3
View File
@@ -18,7 +18,7 @@ const WORK_BEGIN := 0.28
const WORK_END := 0.85 const WORK_END := 0.85
func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -> ActionSelectionResult: func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, all_npcs: Array = []) -> ActionSelectionResult:
if npc.is_dead: if npc.is_dead:
return null return null
@@ -120,10 +120,10 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -
SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander" SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander"
) )
return _choose_best_work_action(npc, village) return _choose_best_work_action(npc, village, all_npcs)
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult: func _choose_best_work_action(npc: SimNPC, village: SimVillage, all_npcs: Array) -> ActionSelectionResult:
var scores := { var scores := {
SimulationIds.ACTION_GATHER_FOOD: SimulationIds.ACTION_GATHER_FOOD:
_calculate_score( _calculate_score(
@@ -170,6 +170,16 @@ func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelecti
0.75 0.75
) )
} }
for familiar_id in npc.familiarity:
if not familiar_id is int:
continue
var other_task: StringName
for other in all_npcs:
if other.id == familiar_id and not other.is_dead:
other_task = other.current_task
break
if other_task in scores:
scores[other_task] = float(scores[other_task]) + 0.3
var best_action := SimulationIds.ACTION_GATHER_FOOD var best_action := SimulationIds.ACTION_GATHER_FOOD
var best_score: float = scores[best_action] var best_score: float = scores[best_action]
for action_id in [ for action_id in [
+25 -1
View File
@@ -132,12 +132,14 @@ func _refresh_npc_inspector() -> void:
if not score_rows.is_empty(): if not score_rows.is_empty():
score_text = "\n\nUtility\n" + "\n".join(score_rows) score_text = "\n\nUtility\n" + "\n".join(score_rows)
var event_text := _build_event_history(npc) var event_text := _build_event_history(npc)
var housemate_text := _build_housemate_display(npc)
npc_inspector_label.text = ( npc_inspector_label.text = (
"%s · %s\n" "%s · %s\n"
+ "Task: %s (%s)\n" + "Task: %s (%s)\n"
+ "Destination: %s\n" + "Destination: %s\n"
+ "Hunger: %.0f Energy: %.0f\n" + "Hunger: %.0f Energy: %.0f\n"
+ "Carrying food: %.0f wood: %.0f\n\n" + "Carrying food: %.0f wood: %.0f\n"
+ "%s\n"
+ "Why\n%s%s\n\n" + "Why\n%s%s\n\n"
+ "Recent\n%s\n\n" + "Recent\n%s\n\n"
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset" + "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
@@ -151,6 +153,7 @@ func _refresh_npc_inspector() -> void:
npc.energy, npc.energy,
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),
housemate_text,
reason_text, reason_text,
score_text, score_text,
event_text event_text
@@ -188,3 +191,24 @@ func _build_event_history(npc: SimNPC) -> String:
for event in events: for event in events:
lines.append(event.description(name_map)) lines.append(event.description(name_map))
return "\n".join(lines) return "\n".join(lines)
func _build_housemate_display(npc: SimNPC) -> String:
if npc.familiarity.is_empty():
return ""
var highest_id: int = -1
var highest_score := -1.0
for key in npc.familiarity:
var other_id: int = int(key)
var score: float = float(npc.familiarity[key])
if score > highest_score:
highest_score = score
highest_id = other_id
if highest_id < 0:
return ""
var other_name := "Someone"
for other_npc in simulation_manager.npcs:
if other_npc.id == highest_id:
other_name = other_npc.npc_name
break
return "Familiar: %s (%.0f%%)" % [other_name, highest_score * 100.0]