feat: add npc decision inspector
This commit is contained in:
@@ -101,3 +101,26 @@ theme_override_constants/margin_bottom = 4
|
||||
[node name="VillageStatsLabel" type="Label" parent="UI/VillagePanel/MarginContainer" unique_id=1554319259]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 10
|
||||
|
||||
[node name="NpcInspectorPanel" type="PanelContainer" parent="UI"]
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -370.0
|
||||
offset_top = 20.0
|
||||
offset_right = -20.0
|
||||
offset_bottom = 330.0
|
||||
grow_horizontal = 0
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="UI/NpcInspectorPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 12
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 12
|
||||
|
||||
[node name="NpcInspectorLabel" type="Label" parent="UI/NpcInspectorPanel/MarginContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.96, 0.91, 0.78, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Villager inspector"
|
||||
|
||||
@@ -12,6 +12,9 @@ const DEBUG_LOGS := true
|
||||
@export var stuck_timeout := 1.5
|
||||
|
||||
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
|
||||
@onready var body_mesh: MeshInstance3D = $MeshInstance3D
|
||||
@onready var profession_prop: MeshInstance3D = $ProfessionProp
|
||||
@onready var profession_label: Label3D = $ProfessionLabel
|
||||
@onready var carried_food_visual: MeshInstance3D = $CarriedFood
|
||||
|
||||
var has_reported_arrival := false
|
||||
@@ -45,9 +48,66 @@ func setup_from_sim(npc: SimNPC) -> void:
|
||||
npc_name = npc.npc_name
|
||||
profession = npc.profession
|
||||
name = "NPC_%s_%s" % [sim_id, npc_name]
|
||||
_apply_profession_presentation()
|
||||
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)
|
||||
|
||||
|
||||
func _apply_profession_presentation() -> void:
|
||||
var definition := SimulationDefinitions.get_profession(profession)
|
||||
if definition == null:
|
||||
profession_label.text = npc_name
|
||||
profession_prop.visible = false
|
||||
return
|
||||
|
||||
var body_material := StandardMaterial3D.new()
|
||||
body_material.albedo_color = definition.visual_color
|
||||
body_material.roughness = 0.82
|
||||
body_mesh.material_override = body_material
|
||||
|
||||
var prop_material := StandardMaterial3D.new()
|
||||
prop_material.albedo_color = definition.prop_color
|
||||
prop_material.roughness = 0.78
|
||||
profession_prop.material_override = prop_material
|
||||
profession_prop.mesh = _create_profession_prop_mesh()
|
||||
profession_prop.visible = profession_prop.mesh != null
|
||||
profession_label.text = "%s\n%s" % [npc_name, definition.display_name]
|
||||
|
||||
|
||||
func _create_profession_prop_mesh() -> PrimitiveMesh:
|
||||
profession_prop.position = Vector3(0.48, 0.85, 0.0)
|
||||
profession_prop.rotation_degrees = Vector3.ZERO
|
||||
match profession:
|
||||
SimulationIds.PROFESSION_FARMER:
|
||||
var basket := SphereMesh.new()
|
||||
basket.radius = 0.24
|
||||
basket.height = 0.34
|
||||
return basket
|
||||
SimulationIds.PROFESSION_WOODCUTTER:
|
||||
var tool := BoxMesh.new()
|
||||
tool.size = Vector3(0.12, 0.72, 0.16)
|
||||
profession_prop.position = Vector3(0.48, 0.92, 0.0)
|
||||
profession_prop.rotation_degrees.z = -18.0
|
||||
return tool
|
||||
SimulationIds.PROFESSION_GUARD:
|
||||
var shield := CylinderMesh.new()
|
||||
shield.top_radius = 0.3
|
||||
shield.bottom_radius = 0.3
|
||||
shield.height = 0.12
|
||||
profession_prop.rotation_degrees.x = 90.0
|
||||
return shield
|
||||
SimulationIds.PROFESSION_SCHOLAR:
|
||||
var book := BoxMesh.new()
|
||||
book.size = Vector3(0.42, 0.1, 0.3)
|
||||
return book
|
||||
SimulationIds.PROFESSION_WANDERER:
|
||||
var pack := SphereMesh.new()
|
||||
pack.radius = 0.28
|
||||
pack.height = 0.5
|
||||
profession_prop.position = Vector3(-0.42, 0.86, 0.0)
|
||||
return pack
|
||||
return null
|
||||
|
||||
|
||||
func set_carried_food_visible(is_visible: bool) -> void:
|
||||
carried_food_visual.visible = is_visible and not is_dead_visual
|
||||
|
||||
@@ -179,6 +239,7 @@ func _report_navigation_failure_once() -> void:
|
||||
func apply_dead_visual_state() -> void:
|
||||
is_dead_visual = true
|
||||
carried_food_visual.visible = false
|
||||
profession_prop.visible = false
|
||||
path_request_id += 1
|
||||
velocity = Vector3.ZERO
|
||||
current_path = PackedVector3Array()
|
||||
|
||||
@@ -38,6 +38,18 @@ mesh = SubResource("CapsuleMesh_d844k")
|
||||
transform = Transform3D(0.15, 0, 0, 0, 0.15, 0, 0, 0, 0.35, 0, 1.2, 0.59052056)
|
||||
mesh = SubResource("BoxMesh_d844k")
|
||||
|
||||
[node name="ProfessionProp" type="MeshInstance3D" parent="."]
|
||||
|
||||
[node name="ProfessionLabel" type="Label3D" parent="."]
|
||||
position = Vector3(0, 2.05, 0)
|
||||
billboard = 1
|
||||
no_depth_test = true
|
||||
text = "Villager"
|
||||
font_size = 24
|
||||
outline_size = 6
|
||||
modulate = Color(1, 0.96, 0.82, 1)
|
||||
pixel_size = 0.007
|
||||
|
||||
[node name="CarriedFood" type="MeshInstance3D" parent="."]
|
||||
visible = false
|
||||
transform = Transform3D(0.75, 0, 0, 0, 0.75, 0, 0, 0, 0.75, 0.48, 0.7, 0)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -53,6 +53,17 @@ func _test_selection_and_execution_are_separate() -> void:
|
||||
selection.action_id == SimulationIds.ACTION_WITHDRAW_FOOD,
|
||||
"Selection should independently choose food retrieval"
|
||||
)
|
||||
_check(
|
||||
not selection.reason.is_empty(),
|
||||
"Selection should expose the real decision branch for presentation"
|
||||
)
|
||||
npc.hunger = 20.0
|
||||
npc.energy = 80.0
|
||||
var work_selection := selector.select_action(npc, village)
|
||||
_check(
|
||||
work_selection.scores.size() == 4,
|
||||
"Ordinary work selection should expose all compared utility scores"
|
||||
)
|
||||
|
||||
|
||||
func _test_target_resolution_and_travel_are_separate() -> void:
|
||||
|
||||
@@ -16,10 +16,38 @@ func _run() -> void:
|
||||
|
||||
var manager: Node = main_scene.get_node("SimulationManager")
|
||||
var view: Node = main_scene.get_node("WorldViewManager")
|
||||
var ui: CanvasLayer = main_scene.get_node("UI")
|
||||
manager.set_process(false)
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
var visual: Node3D = view.active_npc_visuals[npc.id]
|
||||
visual.set_physics_process(false)
|
||||
var profession_definition := SimulationDefinitions.get_profession(npc.profession)
|
||||
var body: MeshInstance3D = visual.get_node("MeshInstance3D")
|
||||
_check(
|
||||
body.material_override.albedo_color == profession_definition.visual_color,
|
||||
"NPC body color should come from its profession definition"
|
||||
)
|
||||
_check(
|
||||
visual.get_node("ProfessionProp").mesh != null
|
||||
and profession_definition.display_name in visual.get_node("ProfessionLabel").text,
|
||||
"NPC visual should expose a profession prop and readable label"
|
||||
)
|
||||
var test_decision := ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_GATHER_FOOD,
|
||||
-1.0,
|
||||
"Test shortage reason",
|
||||
{SimulationIds.ACTION_GATHER_FOOD: 4.5}
|
||||
)
|
||||
manager.latest_decisions[npc.id] = test_decision
|
||||
manager.emit_signal("npc_decision_recorded", npc, test_decision)
|
||||
var inspector_label: Label = ui.get_node(
|
||||
"NpcInspectorPanel/MarginContainer/NpcInspectorLabel"
|
||||
)
|
||||
_check(
|
||||
"Test shortage reason" in inspector_label.text
|
||||
and npc.npc_name in inspector_label.text,
|
||||
"NPC inspector should show the selected villager's real decision reason"
|
||||
)
|
||||
var carried_food: MeshInstance3D = visual.get_node("CarriedFood")
|
||||
_check(not carried_food.visible, "NPC should begin without a carried-food prop")
|
||||
npc.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
|
||||
@@ -38,11 +38,19 @@ func _test_registry_integrity() -> void:
|
||||
|
||||
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||
_check(profession_ids.size() == 5, "Registry should contain all five prototype professions")
|
||||
var profession_colors := {}
|
||||
for profession_id in profession_ids:
|
||||
var profession := SimulationDefinitions.get_profession(profession_id)
|
||||
_check(
|
||||
SimulationDefinitions.get_profession(profession_id) != null,
|
||||
profession != null,
|
||||
"Profession lookup should return '%s'" % profession_id
|
||||
)
|
||||
if profession != null:
|
||||
profession_colors[profession.visual_color.to_html()] = true
|
||||
_check(
|
||||
profession_colors.size() == profession_ids.size(),
|
||||
"Each profession should have a distinct definition-backed visual color"
|
||||
)
|
||||
|
||||
|
||||
func _test_definition_backed_behavior() -> void:
|
||||
|
||||
@@ -5,6 +5,10 @@ const DEBUG_LOGS := false
|
||||
@export var simulation_manager: Node
|
||||
|
||||
@onready var village_stats_label: Label = $VillagePanel/MarginContainer/VillageStatsLabel
|
||||
@onready var npc_inspector_label: Label = $NpcInspectorPanel/MarginContainer/NpcInspectorLabel
|
||||
|
||||
var selected_npc_index := 0
|
||||
var inspector_refresh_accumulator := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -16,9 +20,34 @@ func _ready() -> void:
|
||||
simulation_manager.village_changed.connect(_on_village_changed)
|
||||
else:
|
||||
push_error("VillageUI: SimulationManager has no village_changed signal")
|
||||
if simulation_manager.has_signal("npc_decision_recorded"):
|
||||
simulation_manager.npc_decision_recorded.connect(_on_npc_decision_recorded)
|
||||
else:
|
||||
push_error("VillageUI: SimulationManager has no npc_decision_recorded signal")
|
||||
if simulation_manager.has_signal("state_restored"):
|
||||
simulation_manager.state_restored.connect(_on_state_restored)
|
||||
|
||||
if "village" in simulation_manager:
|
||||
_on_village_changed(simulation_manager.village)
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
inspector_refresh_accumulator += delta
|
||||
if inspector_refresh_accumulator < 0.25:
|
||||
return
|
||||
inspector_refresh_accumulator = 0.0
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if not event is InputEventKey or not event.pressed or event.echo:
|
||||
return
|
||||
if event.keycode != KEY_TAB or simulation_manager.npcs.is_empty():
|
||||
return
|
||||
selected_npc_index = (selected_npc_index + 1) % simulation_manager.npcs.size()
|
||||
_refresh_npc_inspector()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_village_changed(village: SimVillage) -> void:
|
||||
@@ -47,3 +76,73 @@ func _on_village_changed(village: SimVillage) -> void:
|
||||
village.knowledge_modifier
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
func _on_npc_decision_recorded(_npc: SimNPC, _decision: ActionSelectionResult) -> void:
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _on_state_restored() -> void:
|
||||
selected_npc_index = 0
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _refresh_npc_inspector() -> void:
|
||||
if simulation_manager == null or simulation_manager.npcs.is_empty():
|
||||
npc_inspector_label.text = "No villagers available"
|
||||
return
|
||||
selected_npc_index = clampi(selected_npc_index, 0, simulation_manager.npcs.size() - 1)
|
||||
var npc: SimNPC = simulation_manager.npcs[selected_npc_index]
|
||||
var profession_definition := SimulationDefinitions.get_profession(npc.profession)
|
||||
var profession_name := String(npc.profession)
|
||||
if profession_definition != null:
|
||||
profession_name = profession_definition.display_name
|
||||
var action_name := _get_action_name(npc.current_task)
|
||||
var destination_name := "Resolving"
|
||||
if not npc.target_id.is_empty():
|
||||
destination_name = String(npc.target_id)
|
||||
var decision: ActionSelectionResult = simulation_manager.get_latest_decision(npc.id)
|
||||
var reason_text := "Awaiting the next decision"
|
||||
var score_text := ""
|
||||
if decision != null:
|
||||
reason_text = decision.reason
|
||||
var score_keys: Array = decision.scores.keys()
|
||||
score_keys.sort()
|
||||
var score_rows: Array[String] = []
|
||||
for action_id in score_keys:
|
||||
score_rows.append(
|
||||
"%s %.2f" % [_get_action_name(StringName(action_id)), float(decision.scores[action_id])]
|
||||
)
|
||||
if not score_rows.is_empty():
|
||||
score_text = "\n\nUtility\n" + "\n".join(score_rows)
|
||||
npc_inspector_label.text = (
|
||||
"%s · %s\n"
|
||||
+ "Task: %s (%s)\n"
|
||||
+ "Destination: %s\n"
|
||||
+ "Hunger: %.0f Energy: %.0f\n"
|
||||
+ "Carrying food: %.0f\n\n"
|
||||
+ "Why\n%s%s\n\n"
|
||||
+ "[Tab] Next villager"
|
||||
) % [
|
||||
npc.npc_name,
|
||||
profession_name,
|
||||
action_name,
|
||||
npc.task_state,
|
||||
destination_name,
|
||||
npc.hunger,
|
||||
npc.energy,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||
reason_text,
|
||||
score_text
|
||||
]
|
||||
|
||||
|
||||
func _get_action_name(action_id: StringName) -> String:
|
||||
if action_id == SimulationIds.ACTION_IDLE:
|
||||
return "Idle"
|
||||
if action_id == SimulationIds.ACTION_DEAD:
|
||||
return "Dead"
|
||||
var definition := SimulationDefinitions.get_action(action_id)
|
||||
if definition != null:
|
||||
return definition.display_name
|
||||
return String(action_id).capitalize()
|
||||
|
||||
Reference in New Issue
Block a user