feat: add npc decision inspector

This commit is contained in:
2026-07-05 23:50:18 +02:00
parent 7edf9acd57
commit d883148132
16 changed files with 305 additions and 13 deletions
+9
View File
@@ -8,6 +8,7 @@ signal npc_travel_requested(npc: SimNPC, target_position: Vector3)
signal npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float)
signal economic_event_recorded(event: EconomicEventRecord)
signal state_restored
signal npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult)
var village := SimVillage.new()
@@ -24,6 +25,7 @@ var resource_states: Dictionary = {}
var storage_states: Dictionary = {}
var economic_events: Array[EconomicEventRecord] = []
var next_event_id := 0
var latest_decisions: Dictionary = {}
var action_selector := ActionSelectionSystem.new()
var action_executor := ActionExecutionSystem.new()
var target_resolver := ActionTargetResolver.new()
@@ -111,6 +113,8 @@ func simulate_tick() -> void:
):
var selection := action_selector.select_action(npc, village)
if selection != null:
latest_decisions[npc.id] = selection
npc_decision_recorded.emit(npc, selection)
npc.set_task(selection.action_id, selection.duration_override)
if old_task != npc.current_task and old_target != &"":
@@ -679,6 +683,10 @@ func get_state_checksum() -> String:
return create_state_record().to_json().sha256_text()
func get_latest_decision(npc_id: int) -> ActionSelectionResult:
return latest_decisions.get(npc_id)
func create_state_record() -> SimulationStateRecord:
var record := SimulationStateRecord.new()
var wander_streams: Array[Dictionary] = []
@@ -747,6 +755,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
storage_states[storage_record.get_storage_id()] = storage_record
_sync_village_food()
economic_events = record.economic_events.duplicate()
latest_decisions.clear()
npcs.clear()
for npc_record in record.npcs:
+10 -1
View File
@@ -3,8 +3,17 @@ extends RefCounted
var action_id: StringName
var duration_override := -1.0
var reason: String
var scores: Dictionary
func _init(selected_action_id: StringName, selected_duration_override: float = -1.0) -> void:
func _init(
selected_action_id: StringName,
selected_duration_override: float = -1.0,
decision_reason: String = "",
decision_scores: Dictionary = {}
) -> void:
action_id = selected_action_id
duration_override = selected_duration_override
reason = decision_reason
scores = decision_scores.duplicate(true)
+29 -11
View File
@@ -7,24 +7,40 @@ func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
return null
if npc.is_starving:
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
return ActionSelectionResult.new(SimulationIds.ACTION_EAT, 1.0)
return ActionSelectionResult.new(
SimulationIds.ACTION_EAT, 1.0, "Starving and carrying food"
)
if village.food > 0:
return ActionSelectionResult.new(SimulationIds.ACTION_WITHDRAW_FOOD, 1.0)
return ActionSelectionResult.new(SimulationIds.ACTION_GATHER_FOOD, 4.0)
return ActionSelectionResult.new(
SimulationIds.ACTION_WITHDRAW_FOOD, 1.0, "Starving; pantry has food"
)
return ActionSelectionResult.new(
SimulationIds.ACTION_GATHER_FOOD, 4.0, "Starving; pantry is empty"
)
if npc.hunger > 75.0:
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
return ActionSelectionResult.new(SimulationIds.ACTION_EAT)
return ActionSelectionResult.new(
SimulationIds.ACTION_EAT, -1.0, "Hungry and carrying food"
)
if village.food > 0:
return ActionSelectionResult.new(SimulationIds.ACTION_WITHDRAW_FOOD)
return ActionSelectionResult.new(SimulationIds.ACTION_GATHER_FOOD)
return ActionSelectionResult.new(
SimulationIds.ACTION_WITHDRAW_FOOD, -1.0, "Hungry; pantry has food"
)
return ActionSelectionResult.new(
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Hungry; pantry is empty"
)
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
return ActionSelectionResult.new(SimulationIds.ACTION_DEPOSIT_FOOD)
return ActionSelectionResult.new(
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
)
if npc.energy < 25.0:
return ActionSelectionResult.new(SimulationIds.ACTION_REST)
return ActionSelectionResult.new(_choose_best_work_action(npc, village))
return ActionSelectionResult.new(
SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold"
)
return _choose_best_work_action(npc, village)
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> StringName:
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
var scores := {
SimulationIds.ACTION_GATHER_FOOD:
_calculate_score(
@@ -80,7 +96,9 @@ func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> StringName:
if score > best_score:
best_action = action_id
best_score = score
return best_action
return ActionSelectionResult.new(
best_action, -1.0, "Highest current work utility", scores
)
func _calculate_score(
@@ -3,6 +3,8 @@ extends Resource
@export var profession_id: StringName
@export var display_name: String
@export var visual_color := Color.WHITE
@export var prop_color := Color(0.35, 0.25, 0.15, 1.0)
func validate() -> Array[String]:
@@ -11,4 +13,6 @@ func validate() -> Array[String]:
errors.append("profession_id is empty")
if display_name.is_empty():
errors.append("display_name is empty for '%s'" % profession_id)
if visual_color.a <= 0.0:
errors.append("visual_color is transparent for '%s'" % profession_id)
return errors
@@ -6,3 +6,5 @@
script = ExtResource("1")
profession_id = &"farmer"
display_name = "Farmer"
visual_color = Color(0.36, 0.62, 0.28, 1)
prop_color = Color(0.65, 0.45, 0.2, 1)
@@ -6,3 +6,5 @@
script = ExtResource("1")
profession_id = &"guard"
display_name = "Guard"
visual_color = Color(0.25, 0.42, 0.68, 1)
prop_color = Color(0.72, 0.75, 0.78, 1)
@@ -6,3 +6,5 @@
script = ExtResource("1")
profession_id = &"scholar"
display_name = "Scholar"
visual_color = Color(0.52, 0.34, 0.68, 1)
prop_color = Color(0.78, 0.66, 0.28, 1)
@@ -6,3 +6,5 @@
script = ExtResource("1")
profession_id = &"wanderer"
display_name = "Wanderer"
visual_color = Color(0.72, 0.57, 0.3, 1)
prop_color = Color(0.38, 0.28, 0.19, 1)
@@ -6,3 +6,5 @@
script = ExtResource("1")
profession_id = &"woodcutter"
display_name = "Woodcutter"
visual_color = Color(0.67, 0.34, 0.2, 1)
prop_color = Color(0.34, 0.22, 0.14, 1)