Files
gamedev-the-steward/world/ui/ui.gd
T
2026-07-11 19:58:47 +02:00

310 lines
9.6 KiB
GDScript
Raw 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
@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 "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 _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 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"
+ "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,
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 := ""
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)"
return "Known fact: %s%s" % [latest_event.description(name_map), provenance]