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:
@@ -127,7 +127,7 @@ func simulate_tick() -> void:
|
||||
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]
|
||||
):
|
||||
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:
|
||||
latest_decisions[npc.id] = 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.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:
|
||||
print(
|
||||
"[SimulationManager] ",
|
||||
|
||||
@@ -18,7 +18,7 @@ const WORK_BEGIN := 0.28
|
||||
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:
|
||||
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"
|
||||
)
|
||||
|
||||
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 := {
|
||||
SimulationIds.ACTION_GATHER_FOOD:
|
||||
_calculate_score(
|
||||
@@ -170,6 +170,16 @@ func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelecti
|
||||
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_score: float = scores[best_action]
|
||||
for action_id in [
|
||||
|
||||
+25
-1
@@ -132,12 +132,14 @@ func _refresh_npc_inspector() -> void:
|
||||
if not score_rows.is_empty():
|
||||
score_text = "\n\nUtility\n" + "\n".join(score_rows)
|
||||
var event_text := _build_event_history(npc)
|
||||
var housemate_text := _build_housemate_display(npc)
|
||||
npc_inspector_label.text = (
|
||||
"%s · %s\n"
|
||||
+ "Task: %s (%s)\n"
|
||||
+ "Destination: %s\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"
|
||||
+ "Recent\n%s\n\n"
|
||||
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
||||
@@ -151,6 +153,7 @@ func _refresh_npc_inspector() -> void:
|
||||
npc.energy,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
||||
housemate_text,
|
||||
reason_text,
|
||||
score_text,
|
||||
event_text
|
||||
@@ -188,3 +191,24 @@ func _build_event_history(npc: SimNPC) -> String:
|
||||
for event in events:
|
||||
lines.append(event.description(name_map))
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user