Lots of little tweaks and fixes
This commit is contained in:
@@ -17,7 +17,7 @@ Never instantiate these — access only via the global handle.
|
||||
| `QUEST` | Quest management (stub) |
|
||||
| `CUTSCENE` | Cutscene global (stub) |
|
||||
| `DialogueManager` | godot_dialogue_manager v3.10.4 — parses and steps through `.dialogue` files |
|
||||
| `UI` | Root UI accessor — `UI.TEXTBOX`, `UI.DEBUG_MENU`, `UI.GAME_MENU` |
|
||||
| `UI` | Root UI accessor — `UI.PAUSE_MENU`, `UI.QUIT_DIALOG`, `UI.MAIN_MENU_DIALOG`, `UI.BACKDROP`, `UI.DEBUG_MENU`, `UI.GAME_MENU` |
|
||||
|
||||
## Scene Graph
|
||||
|
||||
@@ -25,7 +25,13 @@ Never instantiate these — access only via the global handle.
|
||||
RootScene (Node3D)
|
||||
└─ overworld / battle / cooking / initial ← one shown at a time
|
||||
RootUI (Control, always visible)
|
||||
└─ VNTextbox, DebugMenu, PauseMenu, GameMenu
|
||||
├─ DebugMenu
|
||||
├─ GameMenu
|
||||
├─ ChatBoxContainer (world-space dialogue textboxes)
|
||||
├─ ModalBackdrop (repositions dynamically — see ui.md)
|
||||
├─ PauseMenu
|
||||
├─ QuitConfirmDialog
|
||||
└─ MainMenuConfirmDialog
|
||||
```
|
||||
|
||||
`RootScene` listens to `SCENE.sceneChanged` and shows/hides the correct sub-tree.
|
||||
|
||||
+10
-10
@@ -118,14 +118,14 @@ Whether a line of dialogue is triggered by:
|
||||
|
||||
### Dialogue and movement control
|
||||
|
||||
Dialogue does **not** automatically block movement. Each dialogue sequence declares whether it pauses movement:
|
||||
Dialogue does **not** automatically block movement or camera. Each dialogue sequence declares whether it pauses them:
|
||||
|
||||
- **Blocking** (most NPC conversations triggered by player): sets `UI.dialogueActive = true` for the duration; `EntityMovement._canMove()` returns false.
|
||||
- **Non-blocking** (ambient chatter, background NPC conversations, timed popups): dialogue runs without setting `dialogueActive`; the player can move freely.
|
||||
- **Blocking** (most NPC conversations triggered by player): sets `UI.activeConversation = true` for the duration; `EntityMovement._canMove()` returns false and `OverworldCamera._canOrbit()` returns false.
|
||||
- **Non-blocking** (ambient chatter, background NPC conversations, timed popups): dialogue runs without setting `activeConversation`; the player can move and orbit the camera freely.
|
||||
|
||||
This is configured per `DialogueAction` call, not per line.
|
||||
|
||||
> **Implemented:** `DialogueMode.CONVERSATION` sets `UI.activeConversation = true` (blocks movement). `NARRATION` and `AMBIENT` are non-blocking. `UI.dialogueActive` is driven by `DialogueManager.dialogue_started/ended` signals and is true for any running dialogue regardless of mode.
|
||||
> **Implemented:** `DialogueMode.CONVERSATION` sets `UI.activeConversation = true` (blocks movement and camera orbit). `NARRATION` and `AMBIENT` are non-blocking. `UI.dialogueActive` is driven by `DialogueManager.dialogue_started/ended` signals (emitted by `DialogueAction`) and is true for any running dialogue regardless of mode.
|
||||
|
||||
### Text reveal (scrolling)
|
||||
|
||||
@@ -185,13 +185,13 @@ The choice textbox follows the same world-space anchor and screen-edge clamping
|
||||
|
||||
Every dialogue sequence has a `DialogueMode` that controls movement blocking and advancement behaviour. Set it per `DialogueAction` call — not per line.
|
||||
|
||||
| Mode | Movement | Advancement | Typical use |
|
||||
|---|---|---|---|
|
||||
| `CONVERSATION` | Blocked | Player (Interact) | NPC interactions, cutscene dialogue |
|
||||
| `NARRATION` | Non-blocking | Player (Interact) | Item pickups, announcements the player can dismiss when ready |
|
||||
| `AMBIENT` | Non-blocking | Timed (auto) | Background NPC-to-NPC chatter, timed popups |
|
||||
| Mode | Movement | Camera orbit | Advancement | Typical use |
|
||||
|---|---|---|---|---|
|
||||
| `CONVERSATION` | Blocked | Locked | Player (Interact) | NPC interactions, cutscene dialogue |
|
||||
| `NARRATION` | Non-blocking | Free | Player (Interact) | Item pickups, announcements the player can dismiss when ready |
|
||||
| `AMBIENT` | Non-blocking | Free | Timed (auto) | Background NPC-to-NPC chatter, timed popups |
|
||||
|
||||
`UI.dialogueActive` is driven by `DialogueManager.dialogue_started` / `dialogue_ended` signals and is true whenever any dialogue is running, regardless of mode. Movement blocking is checked separately: `EntityMovement._canMove()` is false only when an active `CONVERSATION` sequence is in progress.
|
||||
`UI.dialogueActive` is driven by `DialogueManager.dialogue_started` / `dialogue_ended` signals (emitted by `DialogueAction`) and is true whenever any dialogue is running, regardless of mode. Movement and camera blocking are checked separately via `UI.activeConversation`: `EntityMovement._canMove()` and `OverworldCamera._canOrbit()` both return false only when an active `CONVERSATION` sequence is in progress.
|
||||
|
||||
More modes can be added to this enum as new use cases arise.
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ Camera-relative direction is derived from the active `Camera3D`'s basis — the
|
||||
| `centeredPitch:float` | `30.0` | Target pitch in CENTERED mode |
|
||||
| `collisionMask:int` | `1` | Physics layers the camera avoids; entities are on layer 2 and excluded by default |
|
||||
|
||||
**Input lock:** `_canOrbit()` returns false when `UI.activeConversation` is true. All manual orbit input (controller stick, right-click drag, `center_camera`) is suppressed. If right-click was held when a conversation starts, the mouse is released automatically. The camera stays at its current position and the positioning math still runs, so it remains correctly placed relative to the (non-moving) player.
|
||||
|
||||
**Mode transitions:**
|
||||
- FREE → CENTERED: after `centeredDelay` seconds of player movement with no camera input, or immediately via `center_camera` (G / LB)
|
||||
- CENTERED → FREE: any manual camera input (controller stick or right-click drag)
|
||||
|
||||
@@ -12,4 +12,4 @@ These exist in the codebase but have no real implementation yet. Don't assume th
|
||||
| `BattleItem.perform()` | Stub |
|
||||
| `CookingScene.tscn` | Placeholder UI only |
|
||||
| Response branching in `DialogueAction` | Auto-selects first allowed response; no response UI exists yet |
|
||||
| `Pause.gd` | Logic commented out |
|
||||
| `Pause.gd` | Wired — opens/closes `UI.PAUSE_MENU`; blocked on `INITIAL` scene |
|
||||
|
||||
+113
-53
@@ -5,68 +5,44 @@
|
||||
```
|
||||
RootUI (Control, fullscreen, always visible)
|
||||
├── DebugMenu
|
||||
├── PauseMenu
|
||||
│ ├── PauseSettings
|
||||
│ └── PauseMain
|
||||
├── GameMenu
|
||||
│ ├── GameMenuPartyTab
|
||||
│ └── GameMenuItemsTab
|
||||
└── VNTextbox
|
||||
├── ChatBoxContainer
|
||||
│ └── InteractIndicator
|
||||
├── ModalBackdrop ← shared backdrop; repositions dynamically
|
||||
├── PauseMenu
|
||||
│ ├── PauseMain
|
||||
│ └── PauseSettings
|
||||
├── QuitConfirmDialog
|
||||
└── MainMenuConfirmDialog
|
||||
```
|
||||
|
||||
`RootUI` is a permanent child of `RootScene` and registers itself with the `UI` singleton on `_enter_tree`. Access its children via `UI.TEXTBOX`, `UI.DEBUG_MENU`, and `UI.GAME_MENU`.
|
||||
Child order matters — later siblings render on top. `ModalBackdrop` shifts its own position in the tree at runtime to sit just below whichever modal overlay is currently topmost (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 | Returns | Notes |
|
||||
| Accessor | Type | Notes |
|
||||
|---|---|---|
|
||||
| `UI.TEXTBOX` | `VNTextbox` | Bottom-screen dialogue box |
|
||||
| `UI.DEBUG_MENU` | `DebugMenu` | Dev scene-jump overlay |
|
||||
| `UI.GAME_MENU` | `GameMenu` | JRPG-style in-game menu |
|
||||
| `UI.dialogueActive` | `bool` | `true` for the entire duration of a `DialogueAction`, including line transitions |
|
||||
| `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.BACKDROP` | `ModalBackdrop` | Shared semi-transparent backdrop; managed automatically |
|
||||
| `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 `DialogueAction` — it is broader than just "textbox visible." Movement blocks on both flags: `UI.dialogueActive` prevents movement even while the textbox is briefly hidden between lines.
|
||||
|
||||
## VNTextbox
|
||||
|
||||
Bottom-anchored `PanelContainer` that reveals text character-by-character and paginates when content overflows 4 lines.
|
||||
|
||||
**Showing text from code:**
|
||||
|
||||
```gdscript
|
||||
# Fire-and-forget
|
||||
UI.TEXTBOX.setText("Hello world.")
|
||||
|
||||
# Await player dismiss — use this in cutscene callables
|
||||
await UI.TEXTBOX.setTextAndWait("Hello world.")
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
1. `setText` resets reveal state and sets new text; textbox becomes visible automatically
|
||||
2. Player holds `interact` to speed up reveal; press again after reveal completes to advance page or close
|
||||
3. `textboxClosing` signal fires when the last page is dismissed
|
||||
4. `setTextAndWait` awaits that signal before returning
|
||||
|
||||
**Input guard:** `EntityMovement._canMove()` returns `false` while `!UI.TEXTBOX.isClosed`. Don't set text without also expecting movement to be blocked.
|
||||
|
||||
**Signal:** `textboxClosing` — emitted once per `setTextAndWait` call when player dismisses.
|
||||
|
||||
## AdvancedRichText
|
||||
|
||||
`RichTextLabel` subclass (`@tool`) used inside `VNTextbox`. 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`.
|
||||
`dialogueActive` is set by `DialogueManager` signals — it is broader than any single textbox being visible. Movement blocks on `dialogueActive` and on `UI.GAME_MENU.isOpen()`.
|
||||
|
||||
## ClosableMenu
|
||||
|
||||
Base class for any togglable panel. Extends `Control`; `isOpen` drives `visible`.
|
||||
Base class for any togglable panel. Extends `Control`; the `isOpen:bool` export drives `visible`.
|
||||
|
||||
```gdscript
|
||||
menu.open() # shows, emits opened
|
||||
@@ -78,6 +54,67 @@ Signals: `opened`, `closed`.
|
||||
|
||||
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. Register it with the backdrop in `RootUI._ready()`: `modalBackdrop.register(myDialog)`
|
||||
5. Expose via `RootUI.gd` export + `UISingleton.gd` accessor if other systems need it
|
||||
6. Connect `myDialog.confirmed` wherever the action should fire
|
||||
|
||||
## ModalBackdrop
|
||||
|
||||
`res://ui/component/ModalBackdrop.gd` — a single fullscreen semi-transparent `ColorRect` shared across all modal overlays.
|
||||
|
||||
**How it works:** each registered overlay's `opened`/`closed` signals are connected. When any overlay opens, the backdrop becomes visible and `move_child()`s itself in the scene tree to sit immediately before the highest-index open overlay. When all overlays close, it hides.
|
||||
|
||||
**Result:** only one backdrop is ever visible, and it always sits between the game world (or lower overlays) and the frontmost open overlay — even when overlays are stacked (e.g. `QuitConfirmDialog` over `PauseMenu`).
|
||||
|
||||
**Registering a new overlay:**
|
||||
|
||||
```gdscript
|
||||
# In RootUI._ready() — overlay must be a direct child of RootUI and have opened/closed signals
|
||||
modalBackdrop.register(myOverlay)
|
||||
```
|
||||
|
||||
`PauseMenu`, `QuitConfirmDialog`, and `MainMenuConfirmDialog` are all registered.
|
||||
|
||||
## 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()`.
|
||||
@@ -107,17 +144,39 @@ To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under
|
||||
|
||||
## Pause menu
|
||||
|
||||
`PauseMenu` wraps `PauseMain` (item list) and `PauseSettings` (settings tabs).
|
||||
`PauseMenu` wraps `PauseMain` (button list) and `PauseSettings` (settings tabs). Opening it calls `get_tree().paused = true`; closing restores it.
|
||||
|
||||
| Method | Effect |
|
||||
|---|---|
|
||||
| `PauseMenu.open()` | Shows container, opens PauseMain |
|
||||
| `PauseMenu.close()` | Hides everything |
|
||||
| `PauseMenu.open()` | Pauses tree, shows container, opens PauseMain, emits `opened` |
|
||||
| `PauseMenu.close()` | Unpauses tree, hides everything, emits `closed` |
|
||||
| `PauseMenu.isOpen() -> bool` | Visibility state |
|
||||
|
||||
`ui_cancel` inside the pause menu: if PauseSettings is open, closes it and reopens PauseMain; otherwise closes the whole menu.
|
||||
**`ui_cancel` behaviour inside PauseMenu:**
|
||||
- If `QuitConfirmDialog` or `MainMenuConfirmDialog` is open → ignored (the dialog handles it)
|
||||
- If `PauseSettings` is open → closes settings, reopens PauseMain
|
||||
- Otherwise → closes PauseMenu
|
||||
|
||||
> **Stub:** `Pause.gd` (the singleton) has its logic commented out. Pause menu is not yet wired to actual game-pause state.
|
||||
**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 cancels, focus returns to the button that opened it.
|
||||
|
||||
**Cannot open on main menu:** `Pause.gd` checks `SCENE.currentScene == INITIAL` and skips opening when already closed.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -134,7 +193,7 @@ To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under
|
||||
| Cooking | `SCENE.setScene(COOKING)` |
|
||||
| Initial | `SCENE.setScene(INITIAL)` |
|
||||
|
||||
Access via `UI.DEBUG_MENU`. Starts hidden; `isClosed` getter/setter controls visibility.
|
||||
Access via `UI.DEBUG_MENU`. Starts hidden.
|
||||
|
||||
## Theme & assets
|
||||
|
||||
@@ -145,6 +204,7 @@ Access via `UI.DEBUG_MENU`. Starts hidden; `isClosed` getter/setter controls vis
|
||||
## Adding a new menu
|
||||
|
||||
1. Create a scene whose root extends `ClosableMenu` (or `Control` if open/close isn't needed)
|
||||
2. Add it as a child of `RootUI.tscn`
|
||||
2. Add it as a child of `RootUI.tscn` — position after `PauseMenu` if it should render above it
|
||||
3. Export a typed reference on `RootUI.gd` and wire it in the Inspector
|
||||
4. Expose via a getter on `UISingleton.gd` if other systems need access (follow the `TEXTBOX` / `DEBUG_MENU` pattern)
|
||||
4. Expose via a getter on `UISingleton.gd` if other systems need access (follow the `PAUSE_MENU` / `GAME_MENU` pattern)
|
||||
5. If it needs a backdrop, register it: `modalBackdrop.register(myMenu)` in `RootUI._ready()`
|
||||
|
||||
Reference in New Issue
Block a user