# UI System ## Scene structure ``` RootUI (Control, fullscreen, always visible) ├── DebugMenu ├── GameMenu │ ├── GameMenuPartyTab │ └── GameMenuItemsTab ├── ChatBoxContainer │ └── InteractIndicator ├── ModalBackdrop ← shared backdrop; z_index managed by FocusStack ├── PauseMenu │ ├── PauseMain │ └── PauseSettings ├── QuitConfirmDialog └── MainMenuConfirmDialog ``` Child order matters — later siblings render on top. `ModalBackdrop` stays at a fixed tree position; its `z_index` is driven automatically by the FocusStack (see [ModalBackdrop](#modalbackdrop)). `RootUI` is a permanent child of `RootScene` and registers itself with the `UI` singleton on `_enter_tree`. Access its children through the `UI` singleton. ## UI singleton `UI` (autoload) is the global access point. | Accessor | Type | Notes | |---|---|---| | `UI.DEBUG_MENU` | `DebugMenu` | Dev scene-jump overlay | | `UI.GAME_MENU` | `GameMenu` | JRPG-style in-game menu | | `UI.PAUSE_MENU` | `PauseMenu` | Pause overlay; also pauses the scene tree | | `UI.QUIT_DIALOG` | `QuitConfirmDialog` | "Quit to desktop?" confirm; call `.open()` to show | | `UI.MAIN_MENU_DIALOG` | `ConfirmDialog` | "Return to main menu?" confirm; call `.open()` to show | | `UI.FOCUS_STACK` | `UIFocusStack` | Ordered stack of open `ClosableMenu` layers; only the top layer processes input | | `UI.BACKDROP` | `ModalBackdrop` | Shared semi-transparent backdrop; driven by `FOCUS_STACK.activeLayerChanged` | | `UI.dialogueActive` | `bool` | `true` for the entire duration of a `DialogueAction` | | `UI.activeConversation` | `bool` | `true` only during a `CONVERSATION`-mode dialogue | | `UI.chatBoxContainer` | `Control` | Parent node for world-space dialogue textboxes | `dialogueActive` is set by `DialogueManager` signals — it is broader than any single textbox being visible. Movement and camera orbit block when `UI.FOCUS_STACK.top() != null` or `UI.activeConversation` is true. ## ClosableMenu Base class for all togglable panels. Extends `Control`; the `isOpen:bool` export drives `visible`. ```gdscript menu.open() # shows, pushes to FocusStack (if canClose), emits opened menu.close() # pops from FocusStack (if canClose), hides, emits closed menu.toggle() ``` **Key exports / properties:** | Name | Default | Notes | |---|---|---| | `isOpen:bool` | `false` | Property — read directly (`menu.isOpen`), not a method | | `canClose:bool` | `true` | When true, open/close interact with `UI.FOCUS_STACK` and input is only processed while on top. When false, the menu is passive — shown/hidden externally, never enters the stack. | **Signals:** `opened`, `closed`, `focusGained`, `focusLost`. **Focus management** (only relevant when `canClose = true`): - `_grabInitialFocus()` — virtual; override to place focus on the correct element on first open. - `_savedFocusNode` — focus owner is captured on `_onFocusLost()` and restored on `_onFocusGained()`, so pressing back from a sub-dialog returns focus to the button that opened it. - A `gui_focus_changed` focus trap runs while the layer is on top — if focus escapes to a node outside this layer, it is snapped back. - `set_process_unhandled_input(false)` is set in `_ready()` for canClose menus; input is only re-enabled via `_onFocusGained()` while the layer is on top of the stack. All new menus that need standard show/hide behaviour should extend `ClosableMenu`. ## ConfirmDialog Reusable "Yes / No" confirmation overlay at `res://ui/component/ConfirmDialog.gd`. Extends `ClosableMenu`. ```gdscript dialog.open() # shows, focuses No (safe default), emits opened dialog.confirmed # signal — fires after Yes is pressed, before close ``` **Focus locking:** `focus_neighbor_top/bottom` on both buttons is wired so controller navigation cannot escape the dialog to elements behind it. **`ui_cancel`** closes the dialog the same as pressing No. **Subclassing:** ```gdscript class_name MyConfirmDialog extends ConfirmDialog func _ready() -> void: super._ready() confirmed.connect(func(): do_the_thing()) ``` `QuitConfirmDialog` uses this pattern — it extends `ConfirmDialog` and connects `confirmed → get_tree().quit()` in its own `_ready()`. **Adding a new confirm dialog:** 1. Create a `.tscn` with `ConfirmDialog.gd` as script; set label text in the scene 2. Add `btnYes`/`btnNo` node path exports pointing to your two buttons 3. Add the instance to `RootUI.tscn` after `PauseMenu` (so it renders on top) 4. Expose via `RootUI.gd` export + `UISingleton.gd` accessor if other systems need it 5. Connect `myDialog.confirmed` wherever the action should fire No manual backdrop registration needed — `ModalBackdrop` activates automatically when any `ClosableMenu` in RootUI enters the FocusStack. ## ModalBackdrop `res://ui/component/ModalBackdrop.gd` — a single fullscreen semi-transparent `ColorRect` shared across all modal overlays. **How it works:** `ModalBackdrop` connects to `UI.FOCUS_STACK.activeLayerChanged` in `_ready()`. When a layer becomes active it checks whether that layer is a direct sibling (i.e. a child of RootUI). If yes: backdrop becomes visible, sets `mouse_filter = MOUSE_FILTER_STOP` (blocking all clicks on anything behind it), and sets `z_index = layer.z_index - 5`. If the top layer is from a different parent (e.g. settings inside the main menu scene), or the stack empties, the backdrop hides. **Result:** only one backdrop is ever visible, it always renders between the game world and the frontmost RootUI-level overlay, and it blocks mouse events from reaching anything behind it. No registration is required — `ModalBackdrop` is self-contained. Do not call `register()` on it. ## AdvancedRichText `RichTextLabel` subclass (`@tool`) used inside world-space dialogue textboxes. Handles: - Smart word-wrap (`TextServer.AUTOWRAP_WORD_SMART`) - Pagination via `maxLines` / `startLine` exports - Inline input icons: `[input action=interact]text[/input]` → replaced with the icon image from `res://ui/input/{action}.tres` - Optional auto-translation via Godot's `tr()` Supported icon actions: `interact`, `pause`, `debug`, `up`, `down`, `left`, `right`. ## Game menu JRPG-style in-game menu at `res://ui/gamemenu/`. Open with the `menu` input (**Tab** on keyboard, **Y** on controller). Blocks player movement while open via `EntityMovement._canMove()`. **Structure:** - Left sidebar: `ItemList` tab selector (Party, Items) - Right panel: active tab content | Tab | Content | |---|---| | Party | One card per `PartyMember` — name, status, HP/MP, ATK/DEF/SPD/MAG/LCK | | Items | One row per `ItemStack` in `PARTY.BACKPACK` — item name + quantity | Both tabs are populated dynamically on open; call `refresh()` on the active tab directly if data changes while the menu is already open. **Key members on `GameMenu`:** | Member | Notes | |---|---| | `open()` | Shows menu, refreshes active tab, pushes to FocusStack, grabs sidebar focus | | `close()` | Pops from FocusStack, hides menu | | `isOpen:bool` | Property — read directly, not a method | `ui_cancel` or `menu` closes the menu. `menu` is handled in `_input` (always fires) and opens the menu only when `UI.FOCUS_STACK.top() == null` and `UI.dialogueActive` is false. To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under `ui/gamemenu/`, instance it in `GameMenu.tscn` as a sibling of the other tabs, add an `@export` for it in `GameMenu.gd`, and add the `match` branch in `_selectTab()`. ## Pause menu `PauseMenu` wraps `PauseMain` (button list) and `PauseSettings` (settings tabs). Opening it calls `get_tree().paused = true`; closing restores it. | Member | Notes | |---|---| | `PauseMenu.open()` | Pushes to FocusStack, pauses tree, opens PauseMain, emits `opened` | | `PauseMenu.close()` | Unpauses tree, closes sub-panels, pops from FocusStack, emits `closed` | | `PauseMenu.isOpen:bool` | Property — read directly, not a method | **`ui_cancel` behaviour inside PauseMenu:** - If `PauseSettings` is open → closes settings, reopens PauseMain - Otherwise → closes PauseMenu (`QuitConfirmDialog` and `MainMenuConfirmDialog` sit above PauseMenu on the FocusStack and consume `ui_cancel` themselves — PauseMenu's `_unhandled_input` does not fire while they are open.) **PauseMain buttons:** | Button | Behaviour | |---|---| | Resume | Closes PauseMenu | | Settings | Opens PauseSettings | | Main Menu | Opens `MainMenuConfirmDialog`; on confirm → `SCENE.setScene(INITIAL)` | | Quit Game | Opens `QuitConfirmDialog`; on confirm → `get_tree().quit()` | When either confirm dialog closes, focus automatically returns to the button that opened it via `_savedFocusNode` in the FocusStack. **Cannot open on main menu:** `Pause.gd` checks `SCENE.currentScene == INITIAL` and skips opening. ## Main menu `res://ui/mainmenu/MainMenu.tscn`. Buttons: **New Game**, **Settings**, **Quit Game**. - New Game → `SCENE.setScene(OVERWORLD)` + `OVERWORLD.mapChange(...)` - Settings → opens the `MainMenuSettings` overlay - Quit Game → opens `UI.QUIT_DIALOG`; on cancel, focus returns to the Quit button ## Settings menu `SettingsMenu` is a shared tabbed component instanced in both `MainMenuSettings` and `PauseSettings`. Three tabs: **Gameplay**, **Sound**, **Graphics** — currently contain placeholder labels only. ## Debug menu `DebugMenu` (toggle via `debug` input — **F1**) provides four scene-jump buttons: | Button | Action | |---|---| | Overworld | `SCENE.setScene(OVERWORLD)` | | Battle | `SCENE.setScene(BATTLE)` | | Cooking | `SCENE.setScene(COOKING)` | | Initial | `SCENE.setScene(INITIAL)` | Access via `UI.DEBUG_MENU`. Starts hidden. ## Theme & assets - Global theme: `res://ui/UI Theme.tres` — applied to all UI nodes - Input icons: `res://ui/input/{action}.tres` — one file per action name - Font: configured globally in project settings ## Adding a new menu 1. Create a scene whose root extends `ClosableMenu` (or `Control` if open/close isn't needed) 2. Override `_grabInitialFocus()` to place focus on the first interactive element on open 3. Add it as a child of `RootUI.tscn` — position after `PauseMenu` if it should render above it 4. Export a typed reference on `RootUI.gd` and wire it in the Inspector 5. Expose via a getter on `UISingleton.gd` if other systems need access (follow the `PAUSE_MENU` / `GAME_MENU` pattern) `ModalBackdrop` activates automatically for any `ClosableMenu` that is a direct child of `RootUI`. No registration step needed. Internal sub-panels (like `PauseSettings`) should extend `Control` directly and not enter the FocusStack.