Files
gamedev-the-steward/world/ui/ui.gd
T
admin 6981c7e9bc feat: simulation speed control with bracket keys
Press [ to slow down, ] to speed up through 6 levels: 0.25x, 0.5x, 1x, 2x, 4x, 10x. Clock delta is multiplied by the current speed, so time_of_day advances proportionally and the visual day-night cycle stays synced. Current speed shown in the village panel header. 10x makes a full day cycle watchable in ~24 seconds. Speed changes are non-destructive to simulation determinism.
2026-07-09 00:25:57 +02:00

226 lines
6.6 KiB
GDScript

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()
village_stats_label.text = (
"""
Village [%s]
Food: %s
Wood: %s
Safety: %s
Knowledge: %s
Starving: %s
Food Mod: %.2f
Safety Mod: %.2f
Knowledge Mod: %.2f
%s
"""
% [
speed_text,
round(village.food),
round(village.wood),
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]