extends CanvasLayer ## Top-bar HUD: speed/pause buttons, tick counter, and save/load controls. ## ## Buttons call Sim.set_speed(); active button is yellow-tinted. ## Tick label updates on every EventBus.sim_tick signal. ## Keyboard shortcuts (pause / speed_normal / speed_fast / speed_ultra) are ## handled here so the bar is the single owner of speed-input logic. ## ## Phase 16: SaveBtn → SaveSystem.save_to_slot(&"manual"). ## LoadBtn → opens the LoadMenu CanvasLayer (mounted by main.gd). const ACTIVE_MODULATE := Color(1.2, 1.2, 0.8) const IDLE_MODULATE := Color.WHITE @onready var pause_btn : Button = $Anchor/ButtonRow/PauseBtn @onready var normal_btn : Button = $Anchor/ButtonRow/NormalBtn @onready var fast_btn : Button = $Anchor/ButtonRow/FastBtn @onready var ultra_btn : Button = $Anchor/ButtonRow/UltraBtn @onready var save_btn : Button = $Anchor/ButtonRow/SaveBtn @onready var load_btn : Button = $Anchor/ButtonRow/LoadBtn @onready var tick_label : Label = $Anchor/TickLabel @onready var clock_label : Label = $Anchor/ClockLabel @onready var season_label : Label = $Anchor/SeasonLabel # Maps Speed enum value → the corresponding Button node. var _speed_buttons: Dictionary = {} # Early-out cache: only set label text when the string changes. var _last_clock_text: String = "" var _last_season_text: String = "" ## Injected by main.gd after mount so we don't walk the tree with get_node. var load_menu: CanvasLayer = null ## Phase 17 — injected by main.gd; used by the runtime-added Settings button. var settings_menu: CanvasLayer = null ## Phase 17 (Agent B) — injected by main.gd; used by the runtime-added Build button. var build_drawer: CanvasLayer = null ## Phase 17 (Agent C) — injected by main.gd; used by the runtime-added Work / Log buttons. var work_priority_matrix: CanvasLayer = null var alerts_log_panel: CanvasLayer = null ## Kept to drive the unread badge text. var _log_btn: Button = null func _ready() -> void: var button_row: HBoxContainer = get_node_or_null("Anchor/ButtonRow") if button_row != null: button_row.add_theme_constant_override("separation", 4) pause_btn.text = Strings.t(&"speed.pause") normal_btn.text = Strings.t(&"speed.normal") fast_btn.text = Strings.t(&"speed.fast") ultra_btn.text = Strings.t(&"speed.ultra") save_btn.text = Strings.t(&"ui.save") load_btn.text = Strings.t(&"ui.load") # Phase 19 — desktop hover tooltips (no-op on touch). pause_btn.tooltip_text = Strings.t(&"tooltip.pause") normal_btn.tooltip_text = Strings.t(&"tooltip.speed_normal") fast_btn.tooltip_text = Strings.t(&"tooltip.speed_fast") ultra_btn.tooltip_text = Strings.t(&"tooltip.speed_ultra") save_btn.tooltip_text = Strings.t(&"tooltip.save") load_btn.tooltip_text = Strings.t(&"tooltip.load") tick_label.text = "(boot)" clock_label.text = Strings.t(&"clock.format").format({"d": 1, "t": "06:00"}) season_label.text = Strings.t(&"season.format").format({"s": Strings.t(&"season.spring"), "d": 1}) _speed_buttons = { Sim.Speed.PAUSE: pause_btn, Sim.Speed.NORMAL: normal_btn, Sim.Speed.FAST: fast_btn, Sim.Speed.ULTRA: ultra_btn, } pause_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.PAUSE)) normal_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.NORMAL)) fast_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.FAST)) ultra_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.ULTRA)) save_btn.pressed.connect(_on_save_pressed) load_btn.pressed.connect(_on_load_pressed) EventBus.speed_changed.connect(_on_speed_changed) EventBus.sim_tick.connect(_on_sim_tick) EventBus.sim_tick.connect(_on_clock_refresh) EventBus.sim_tick.connect(_on_season_refresh) EventBus.save_started.connect(_on_save_started) EventBus.save_finished.connect(_on_save_finished) # Reflect the initial speed state without emitting a signal. _apply_highlight(Sim.current_speed) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("pause"): Sim.set_speed(Sim.Speed.PAUSE) elif event.is_action_pressed("speed_normal"): Sim.set_speed(Sim.Speed.NORMAL) elif event.is_action_pressed("speed_fast"): Sim.set_speed(Sim.Speed.FAST) elif event.is_action_pressed("speed_ultra"): Sim.set_speed(Sim.Speed.ULTRA) func _on_speed_changed(new_speed: int) -> void: _apply_highlight(new_speed as Sim.Speed) func _on_sim_tick(tick_number: int) -> void: tick_label.text = Strings.t(&"hud.tick").format({"n": tick_number}) func _on_clock_refresh(_n: int) -> void: var t: String = Strings.t(&"clock.format").format({"d": Clock.current_day(), "t": Clock.time_string()}) if t != _last_clock_text: _last_clock_text = t clock_label.text = t func _on_season_refresh(_n: int) -> void: var season_key: StringName = &"season." + String(Clock.current_season()) var t: String = Strings.t(&"season.format").format({ "s": Strings.t(season_key), "d": Clock.day_of_season() + 1, # display as 1-indexed }) if t != _last_season_text: _last_season_text = t season_label.text = t func _apply_highlight(speed: Sim.Speed) -> void: for s: int in _speed_buttons: _speed_buttons[s].modulate = ACTIVE_MODULATE if s == speed else IDLE_MODULATE func _on_save_pressed() -> void: SaveSystem.save_to_slot(&"manual") Audit.log("top_bar", "manual save triggered") func _on_load_pressed() -> void: if load_menu != null and load_menu.has_method("open"): load_menu.open() else: Audit.log("top_bar", "LoadMenu not mounted — skipping open()") func _on_save_started(_slot: StringName) -> void: save_btn.disabled = true save_btn.text = Strings.t(&"ui.saving") func _on_save_finished(_slot: StringName, _ok: bool) -> void: save_btn.disabled = false save_btn.text = Strings.t(&"ui.save") ## Phase 17 (Agent B) — called by main.gd after build_drawer is injected. ## Appends a Build button to the ButtonRow at runtime so no .tscn edit is needed. func _add_build_btn() -> void: var button_row: HBoxContainer = get_node_or_null("Anchor/ButtonRow") if button_row == null: Audit.log("top_bar", "_add_build_btn: ButtonRow not found — skipping") return var build_btn := Button.new() build_btn.name = "BuildBtn" build_btn.text = "🔨" build_btn.custom_minimum_size = Vector2(40, 40) build_btn.focus_mode = Control.FOCUS_NONE build_btn.tooltip_text = Strings.t(&"tooltip.build") build_btn.pressed.connect(_on_build_pressed) button_row.add_child(build_btn) Audit.log("top_bar", "Build button added to ButtonRow") func _on_build_pressed() -> void: if build_drawer != null and build_drawer.has_method("toggle"): build_drawer.toggle() else: Audit.log("top_bar", "BuildDrawer not mounted — skipping toggle()") ## Phase 17 — called by main.gd after settings_menu is injected. ## Appends a Settings button to the ButtonRow at runtime so no .tscn edit is needed. func _add_settings_btn() -> void: var button_row: HBoxContainer = get_node_or_null("Anchor/ButtonRow") if button_row == null: Audit.log("top_bar", "_add_settings_btn: ButtonRow not found — skipping") return var settings_btn := Button.new() settings_btn.name = "SettingsBtn" settings_btn.text = "⚙" settings_btn.custom_minimum_size = Vector2(40, 40) settings_btn.focus_mode = Control.FOCUS_NONE settings_btn.tooltip_text = Strings.t(&"tooltip.settings") settings_btn.pressed.connect(_on_settings_pressed) button_row.add_child(settings_btn) Audit.log("top_bar", "Settings button added to ButtonRow") func _on_settings_pressed() -> void: if settings_menu != null and settings_menu.has_method("open"): settings_menu.open() else: Audit.log("top_bar", "SettingsMenu not mounted — skipping open()") ## Phase 17 (Agent C) — called by main.gd after work_priority_matrix and ## alerts_log_panel are injected. Appends "Work" and "Log" buttons to ButtonRow. func _add_work_log_btns() -> void: var button_row: HBoxContainer = get_node_or_null("Anchor/ButtonRow") if button_row == null: Audit.log("top_bar", "_add_work_log_btns: ButtonRow not found — skipping") return var work_btn := Button.new() work_btn.name = "WorkBtn" work_btn.text = "👷" work_btn.custom_minimum_size = Vector2(40, 40) work_btn.focus_mode = Control.FOCUS_NONE work_btn.tooltip_text = Strings.t(&"tooltip.work") work_btn.pressed.connect(_on_work_pressed) button_row.add_child(work_btn) _log_btn = Button.new() _log_btn.name = "LogBtn" _log_btn.text = "🔔" _log_btn.custom_minimum_size = Vector2(40, 40) _log_btn.focus_mode = Control.FOCUS_NONE _log_btn.tooltip_text = Strings.t(&"tooltip.log") _log_btn.pressed.connect(_on_log_pressed) button_row.add_child(_log_btn) # Give the AlertsLog a reference to the Log button so it can update the badge. if alerts_log_panel != null and alerts_log_panel.get("log_button") != null: alerts_log_panel.log_button = _log_btn elif alerts_log_panel != null: alerts_log_panel.set("log_button", _log_btn) Audit.log("top_bar", "Work + Log buttons added to ButtonRow") func _on_work_pressed() -> void: if work_priority_matrix != null and work_priority_matrix.has_method("open"): work_priority_matrix.open() else: Audit.log("top_bar", "WorkPriorityMatrix not mounted — skipping open()") func _on_log_pressed() -> void: if alerts_log_panel != null and alerts_log_panel.has_method("open"): alerts_log_panel.open() else: Audit.log("top_bar", "AlertsLog not mounted — skipping open()")