class_name ResumeToast extends CanvasLayer ## Phase 16 — "Welcome back / Load failed" toast. ## ## Layer 22 — above Modal (20) but below LoadMenu (25). ## Listens to EventBus.load_finished. ## On ok=true: "Welcome back — away N minutes/hours" for SHOW_DURATION_SEC, then fades. ## On ok=false: "Load failed (corrupt or version mismatch)" — same fade cadence. const SHOW_DURATION_SEC: float = 5.0 const FADE_DURATION_SEC: float = 0.8 var _root: Control = null var _label: Label = null var _show_timer: Timer = null var _fade_time: float = 0.0 var _fading: bool = false func _ready() -> void: layer = 22 _build_ui() _root.visible = false EventBus.load_finished.connect(_on_load_finished) Audit.log("resume_toast", "ResumeToast ready") func _exit_tree() -> void: if EventBus.load_finished.is_connected(_on_load_finished): EventBus.load_finished.disconnect(_on_load_finished) func _process(delta: float) -> void: if not _fading: return _fade_time -= delta if _fade_time <= 0.0: _fading = false _root.visible = false _root.modulate = Color.WHITE return _root.modulate.a = clampf(_fade_time / FADE_DURATION_SEC, 0.0, 1.0) # ── UI construction ─────────────────────────────────────────────────────────── func _build_ui() -> void: # Top-center strip — sits just below the top bar. _root = Control.new() _root.name = "ToastRoot" _root.set_anchors_preset(Control.PRESET_TOP_WIDE) _root.custom_minimum_size = Vector2(0, 56) _root.offset_top = 56 # below the 48 px top bar + a little gap _root.offset_bottom = 112 _root.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_root) var panel := PanelContainer.new() panel.name = "Panel" panel.set_anchors_preset(Control.PRESET_CENTER_TOP) panel.custom_minimum_size = Vector2(400, 48) panel.offset_left = -200 panel.offset_right = 200 panel.offset_top = 0 panel.offset_bottom = 48 panel.mouse_filter = Control.MOUSE_FILTER_IGNORE _root.add_child(panel) _label = Label.new() _label.name = "ToastLabel" _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _label.autowrap_mode = TextServer.AUTOWRAP_OFF panel.add_child(_label) _show_timer = Timer.new() _show_timer.name = "ShowTimer" _show_timer.one_shot = true _show_timer.wait_time = SHOW_DURATION_SEC _show_timer.timeout.connect(_start_fade) add_child(_show_timer) # ── event handling ──────────────────────────────────────────────────────────── func _on_load_finished(_slot: StringName, ok: bool, real_seconds_away: int) -> void: if ok: _label.text = _format_welcome(real_seconds_away) else: _label.text = Strings.t(&"ui.load_failed") _root.modulate = Color.WHITE _root.visible = true _fading = false _show_timer.start() Audit.log("resume_toast", "showing — ok=%s seconds_away=%d" % [ok, real_seconds_away]) func _start_fade() -> void: _fading = true _fade_time = FADE_DURATION_SEC # ── helpers ─────────────────────────────────────────────────────────────────── func _format_welcome(seconds_away: int) -> String: var duration_str: String if seconds_away < 120: # Less than 2 minutes — show as "1 minute" duration_str = Strings.t(&"ui.welcome_back_min").format({"n": 1}) elif seconds_away < 3600: # Under an hour — show in minutes. var mins: int = seconds_away / 60 var key: StringName = &"ui.welcome_back_min" if mins == 1 else &"ui.welcome_back_mins" duration_str = Strings.t(key).format({"n": mins}) elif seconds_away < 7200: # 1–2 hours exactly → singular. duration_str = Strings.t(&"ui.welcome_back_hour").format({"n": 1}) else: var hrs: int = seconds_away / 3600 var key: StringName = &"ui.welcome_back_hour" if hrs == 1 else &"ui.welcome_back_hours" duration_str = Strings.t(key).format({"n": hrs}) return Strings.t(&"ui.welcome_back").format({"n": duration_str})