G: large_text scales global theme font (14→20 at 1.4×) via new GameState.get_font_scale + EventBus.settings_changed. reduce_motion gates ResumeToast fade (HintOverlay already gated). I: InspectTooltip long-press wired (500ms hold, 12px drift cancel, tap-to-clear pin). Stale Phase 19 TODO replaced with accurate doc. H: Pawn.arrived_at_destination now also emitted on EventBus.pawn_arrived_at_destination; DirtinessSystem subscribes and bumps indoor traffic dirt (BUMP_INDOOR_TRAFFIC = 0.2). Outdoor-tracked bump needs Pawn.prev_tile — flagged for Phase 20. P: CraftingProvider caches ingredient item ref on Job.ingredient_item; JobRunner._tick_pickup validates is_instance_valid + not being_carried before the tile scan, cancels cleanly if another pawn grabbed it. J: rest_provider.gd deleted. Removed @onready + register call from world.gd, ext_resource + node from world.tscn. Provider count comment updated to 9. M: DIRTY_THRESHOLD extracted — cleaning_provider and job_runner now reference DirtinessSystem.DIRT_DIRTY_THRESHOLD. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
129 lines
4.3 KiB
GDScript
129 lines
4.3 KiB
GDScript
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:
|
||
if bool(GameState.settings.get("accessibility_reduce_motion", false)):
|
||
# Snap to hidden immediately — no fade animation.
|
||
_root.visible = false
|
||
_root.modulate = Color.WHITE
|
||
return
|
||
_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})
|