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]