extends CanvasLayer @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 simulation_manager.has_signal("relationship_changed"): simulation_manager.relationship_changed.connect(_on_relationship_changed) if simulation_manager.has_signal("event_knowledge_changed"): simulation_manager.event_knowledge_changed.connect(_on_event_knowledge_changed) if simulation_manager.has_signal("event_knowledge_forgotten"): simulation_manager.event_knowledge_forgotten.connect(_on_event_knowledge_forgotten) 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 _on_relationship_changed( _relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord ) -> void: _refresh_npc_inspector() func _on_event_knowledge_changed(_knower_id: int, _event: EconomicEventRecord) -> void: _refresh_npc_inspector() func _on_event_knowledge_forgotten(_knower_id: int, _event: EconomicEventRecord) -> void: _refresh_npc_inspector() 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] = [] var rejection_rows: Array[String] = [] for action_id in score_keys: if decision.rejections.has(action_id): rejection_rows.append( ( "%s — %s" % [ _get_action_name(StringName(action_id)), String(decision.rejections[action_id]) ] ) ) continue 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) if not rejection_rows.is_empty(): score_text += "\n\nUnavailable\n" + "\n".join(rejection_rows) var event_text := _build_event_history(npc) var relationship_text := _build_relationship_display(npc) var memory_text := _build_memory_display(npc) var known_fact_text := _build_known_fact_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" + "%s\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), relationship_text, memory_text, known_fact_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_relationship_display(npc: SimNPC) -> String: if not simulation_manager.has_method("get_primary_relationship"): return "" var relationship: RelationshipStateRecord = simulation_manager.get_primary_relationship(npc.id) if relationship == null: return "" var other_name := "Someone" for other_npc in simulation_manager.npcs: if other_npc.id == relationship.get_subject_id(): other_name = other_npc.npc_name break var display := ( "Relationship: %s — familiar %.0f%%, trust %.0f%%" % [ other_name, relationship.get_familiarity() * 100.0, relationship.get_trust() * 100.0, ] ) if not simulation_manager.has_method("get_relationship_cause"): return display var cause: EconomicEventRecord = simulation_manager.get_relationship_cause(relationship) if cause == null: return display var name_map := {} for other_npc in simulation_manager.npcs: name_map[other_npc.id] = other_npc.npc_name return "%s\nBecause: %s" % [display, cause.description(name_map)] func _build_known_fact_display(npc: SimNPC) -> String: if not simulation_manager.has_method("get_known_events"): return "" var known_events: Array = simulation_manager.get_known_events(npc.id, 1) if known_events.is_empty(): return "Known fact: none yet" var name_map := {} for other_npc in simulation_manager.npcs: name_map[other_npc.id] = other_npc.npc_name var latest_event := known_events[known_events.size() - 1] as EconomicEventRecord var provenance := "source not recorded" var retention := "recent" if simulation_manager.has_method("get_known_event_record"): var known_record: KnownEventStateRecord = simulation_manager.get_known_event_record( npc.id, int(latest_event.data["event_id"]) ) if known_record != null: match known_record.get_acquisition_method(): SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED: provenance = "performed personally" SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED: provenance = "witnessed" SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED: var source_name: String = name_map.get( known_record.get_source_npc_id(), "someone" ) provenance = "heard from %s" % source_name SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY: provenance = "source not recorded" if ( simulation_manager.has_method("is_known_event_lasting") and (simulation_manager.is_known_event_lasting(npc.id, int(latest_event.data["event_id"]))) ): retention = "lasting" return "Known fact: %s (%s · %s)" % [latest_event.description(name_map), provenance, retention] func _build_memory_display(npc: SimNPC) -> String: if not simulation_manager.has_method("get_memory_summary"): return "" var summary: Dictionary = simulation_manager.get_memory_summary(npc.id) return ( "Memory: %d lasting · %d recent" % [int(summary.get("lasting", 0)), int(summary.get("recent", 0))] )