Files
admin d6520c426a feat: resource consumption rate display in village panel
Village panel now shows daily consumption rates alongside stockpiles (e.g. Food: 15 (-2/d), Wood: 7 (-1/d)). Rates are computed over a rolling 200-tick (~1 day) window by summing item_consumed economic events. The rate display only appears when non-zero, keeping the panel clean until actual consumption occurs. Closes the feedback loop: events show individual transfers, rates show aggregate trends.
2026-07-09 01:18:33 +02:00

235 lines
6.9 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends CanvasLayer
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
var debug_overlay_visible := true
func _ready() -> void:
if simulation_manager == null:
push_error("VillageUI: simulation_manager missing")
return
if simulation_manager.has_signal("village_changed"):
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 simulation_manager.has_signal("speed_changed"):
simulation_manager.speed_changed.connect(_on_speed_changed)
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:
var name_map := {}
for npc in simulation_manager.npcs:
name_map[npc.id] = npc.npc_name
var event_lines: Array[String] = []
for event in simulation_manager.get_recent_events(3):
event_lines.append(event.description(name_map))
var event_text := "\n".join(event_lines)
if event_text.is_empty():
event_text = ""
else:
event_text = "\n" + event_text
var speed_text := ""
if simulation_manager.has_method("get_current_speed"):
speed_text = simulation_manager.get_current_speed()
var rates: Dictionary = simulation_manager.get_resource_rates()
var food_rate := ""
if rates["food_per_day"] > 0.01:
food_rate = " (%.0f/d)" % rates["food_per_day"]
var wood_rate := ""
if rates["wood_per_day"] > 0.01:
wood_rate = " (%.0f/d)" % rates["wood_per_day"]
village_stats_label.text = (
"""
Village [%s]
Food: %s%s
Wood: %s%s
Safety: %s
Knowledge: %s
Starving: %s
Food Mod: %.2f
Safety Mod: %.2f
Knowledge Mod: %.2f
%s
"""
% [
speed_text,
round(village.food),
food_rate,
round(village.wood),
wood_rate,
round(village.safety),
round(village.knowledge),
simulation_manager.get_starving_count(),
village.food_modifier,
village.safety_modifier,
village.knowledge_modifier,
event_text
]
)
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 _on_speed_changed(_multiplier: float) -> void:
if simulation_manager != null and "village" in simulation_manager:
_on_village_changed(simulation_manager.village)
func _refresh_npc_inspector() -> void:
if not debug_overlay_visible:
return
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)
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"
+ "%s\n"
+ "Why\n%s%s\n\n"
+ "Recent\n%s\n\n"
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
) % [
npc.npc_name,
profession_name,
action_name,
npc.task_state,
destination_name,
npc.hunger,
npc.energy,
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
housemate_text,
reason_text,
score_text,
event_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()
func set_debug_overlay_visible(is_visible: bool) -> void:
debug_overlay_visible = is_visible
visible = is_visible
if is_visible:
_refresh_npc_inspector()
func _build_event_history(npc: SimNPC) -> String:
if not simulation_manager.has_method("get_npc_events"):
return ""
var events: Array = simulation_manager.get_npc_events(npc.id, 5)
if events.is_empty():
return "No recorded events yet"
var name_map := {}
for other_npc in simulation_manager.npcs:
name_map[other_npc.id] = other_npc.npc_name
var lines: Array[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]