Improved UI textbox

This commit is contained in:
2026-06-12 11:56:30 -05:00
parent f6a0bb156e
commit 2f3a4eab66
39 changed files with 574 additions and 706 deletions
+46 -24
View File
@@ -102,7 +102,7 @@ The speaker name in a `.dialogue` line (e.g. `Guard: Halt!`) is matched against
- The **display name** shown to the player is a separate property and can differ (e.g. `dialogueName = "guard_captain"` but display name = `"???"` before the character has revealed their name). This lets you write dialogue that references a speaker before the player knows who they are.
- **Every dialogue line must have a speaker.** There is no narrator/no-speaker path — if a line has no natural speaker, use a named invisible anchor Entity in the scene.
> **Not yet implemented:** the entity-lookup and 3D→UI projection logic. Currently `DialogueAction` passes a target node manually.
> **Implemented:** Entity lookup is live — `OVERWORLD.getEntityByDialogueName(line.character)` resolves the speaker. 3D→UI projection runs every frame in `DialogueTextbox._updateWorldPosition()`.
### All conversation routes through DialogueManager
@@ -114,7 +114,7 @@ Whether a line of dialogue is triggered by:
…it is always written in a `.dialogue` file and played back via `DialogueManager.get_next_dialogue_line()`. There is no separate "dumb string" path for simple one-liners.
> **Migration needed:** the current `CHATBOX` interact type (WorldChatBox, plain string) should be replaced with a one-line `.dialogue` file going through the same pipeline.
> **Done:** `CHATBOX` interact type removed. All NPC text now routes through DialogueManager.
### Dialogue and movement control
@@ -125,7 +125,7 @@ Dialogue does **not** automatically block movement. Each dialogue sequence decla
This is configured per `DialogueAction` call, not per line.
> **Partially implemented:** `DialogueAction` always sets `UI.dialogueActive`. The non-blocking path does not exist yet.
> **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.
### Text reveal (scrolling)
@@ -139,11 +139,13 @@ Body text is revealed character-by-character at a speed approximating natural hu
Holding the **Interact** bind speeds up reveal to near-instant. Once all characters on the current page are revealed, pressing Interact advances to the next page or the next dialogue line.
The `[wait=N]` and `[speed=N]` BBCode tags from DialogueManager can override reveal behaviour inline for specific moments (e.g. a shocked line flashing by at `[speed=4]`).
The plugin returns `DialogueLine.text` as a plain BBCode string — it does **not** handle punctuation pauses or `...` detection. All reveal pacing is implemented in our textbox, not the plugin.
**ADA note:** fast reveal speeds (via `[speed=N]` or hold-to-skip) should not flash entire blocks of text in a way that could trigger photosensitive responses. Avoid using `[speed=N]` values so high that multiple lines appear simultaneously. The hold-to-skip path is safe since it requires sustained player input.
The `[wait=N]` and `[speed=N]` BBCode tags are extracted from the text by the plugin during line resolution (stored in `DialogueLine.speeds` and inline mutation data). Our textbox must read these to adjust reveal behaviour inline — e.g. a shocked line using `[speed=4]` to flash past quickly.
> **Partially implemented:** `VNTextbox` has character-by-character reveal and hold-to-skip. Punctuation pause logic is not yet implemented.
**ADA note:** fast reveal speeds (via `[speed=N]` or hold-to-skip) should not flash entire blocks of text in a way that could trigger photosensitive responses. Avoid `[speed=N]` values so high that multiple lines appear simultaneously. The hold-to-skip path is safe since it requires sustained player input.
> **Implemented:** `DialogueTextbox` has character-by-character reveal, punctuation pauses, `...` detection, and hold-to-skip. `[wait=N]` / `[speed=N]` BBCode tag support is not yet wired.
### Advancement modes
@@ -151,12 +153,12 @@ Each dialogue sequence uses one of two advancement modes:
| Mode | Behaviour |
|---|---|
| `PLAYER` (default) | Player presses Interact to advance to the next page or line once reveal is complete. |
| `TIMED` | Each line auto-advances after a delay estimated from line length (reading speed). A per-line `[next=N]` override is supported for lines that need a specific timing. Player input is ignored. |
| `PLAYER` (default) | Player presses Interact to advance to the next page or line once reveal is complete. Used by `CONVERSATION` and `NARRATION` modes. |
| `TIMED` | Each line auto-advances after a delay estimated from line length (reading speed). A per-line `[next=N]` tag overrides the estimated delay. Player input is ignored. Used by `AMBIENT` mode. |
Mixed sequences (some lines timed, some player-advanced) are not supported — the mode is set per conversation, not per line.
Mixed sequences (some lines timed, some player-advanced) are not supported — advancement mode is determined by `DialogueMode`, not per line.
> **Not yet implemented:** `TIMED` mode. Currently all dialogue requires player input.
> **Implemented:** `TIMED` auto-advance is wired in `DialogueTextbox._processAutoAdvance`. Delay is estimated from line length (characters / `READING_CHARS_PER_SECOND`). Per-line `[next=N]` override is not yet wired.
### Response / choice UI
@@ -164,7 +166,7 @@ When a `DialogueLine` has a non-empty `responses` array, reveal pauses and a cho
The choice textbox follows the same world-space anchor and screen-edge clamping rules as all other textboxes.
> **Not yet implemented:** `DialogueAction` currently auto-selects the first allowed response. Replace the auto-select block with a call to the response UI and await its selection signal.
> **Implemented:** `DialogueChoiceBox` is shown when allowed responses exist, anchored to the player entity via `OVERWORLD.getPlayerEntity()`. Player navigates with ui_up/ui_down or move_forward/move_back, confirms with Interact.
### Trigger types
@@ -179,21 +181,37 @@ The choice textbox follows the same world-space anchor and screen-edge clamping
---
## DialogueMode enum
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 |
`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.
More modes can be added to this enum as new use cases arise.
---
## DialogueAction — the Cutscene bridge
[cutscene/dialogue/DialogueAction.gd](../../cutscene/dialogue/DialogueAction.gd) is the glue between the `Cutscene` queue and `DialogueManager`. It fetches lines via `get_next_dialogue_line()`, displays them in the world-space textbox, and returns `CUTSCENE_CONTINUE` when the last line is dismissed.
```gdscript
# Add a blocking dialogue step to any Cutscene
# Add a dialogue step to any Cutscene
cutscene.addCallable(DialogueAction.getDialogueCallable(
load("res://dialogue/npc/test.dialogue"),
"start", # section to begin from
[entity], # extra_game_states exposed to {{variables}} in the file
true # blocking — pauses player movement
"res://dialogue/npc/guard", # base path — no locale suffix, no extension
"start", # section to begin from
[entity], # extra_game_states exposed to {{variables}} in the file
DialogueAction.DialogueMode.CONVERSATION
))
```
The `blockMovement` argument (fourth param, default `true`) controls whether `UI.dialogueActive` is set for the duration.
`DialogueAction` resolves the base path to the correct locale file at runtime (e.g. `guard.en.dialogue`), falling back to `en` if the active locale file is missing.
---
@@ -266,10 +284,12 @@ Set these exports on an Entity node whose `interactType = CONVERSATION`:
| Export | Purpose |
|---|---|
| `dialogueResource:DialogueResource` | The `.dialogue` file to run |
| `dialogueBasePath:String` | Base path to the dialogue file, without locale suffix or extension (e.g. `"res://dialogue/npc/guard"`) |
| `dialogueTitle:String` | Section to start from (default `"start"`) |
| `dialogueName:String` | Key matched against `.dialogue` speaker names to find this entity's textbox anchor |
| `displayName:String` | Player-visible name shown in the textbox speaker label (can differ from `dialogueName`, e.g. `"???"`) |
The entity's `entityId` must match the speaker name used in the dialogue file so the textbox anchor resolves correctly.
`dialogueName` must match the speaker name used in the dialogue file. `entityId` is unrelated to dialogue lookup.
---
@@ -277,9 +297,9 @@ The entity's `entityId` must match the speaker name used in the dialogue file so
| File | Used by |
|---|---|
| `dialogue/npc/test.dialogue` | TestMap NPC (NotPlayer) |
| `dialogue/item/pickup.dialogue` | `ItemAction` — item pickup text with `{{item_name}}` and `{{quantity}}` |
| `dialogue/battle/narration.dialogue` | `BattleCutsceneAction` — move announcements, victory/defeat |
| `dialogue/npc/test.en.dialogue` | TestMap NPC (NotPlayer / "Stranger") |
| `dialogue/item/pickup.en.dialogue` | `ItemAction` — item pickup text with `{{item_name}}` and `{{quantity}}` |
| `dialogue/battle/narration.en.dialogue` | `BattleCutsceneAction` — move announcements, victory/defeat |
---
@@ -292,11 +312,13 @@ dialogue/npc/test.en.dialogue
dialogue/npc/test.ja.dialogue
```
`DialogueAction.getDialogueCallable()` accepts a base path (without locale suffix) and resolves the active locale automatically, falling back to `en` if the locale file is missing.
`DialogueAction.getDialogueCallable()` accepts a base path (no locale suffix, no extension — e.g. `"res://dialogue/npc/test"`) and resolves the active locale automatically via `TranslationServer.get_locale()` (e.g. → `test.en.dialogue`), falling back to `en` if the locale file is missing.
To switch language at runtime: `TranslationServer.set_locale("ja")` — all subsequent dialogue loads will use the new locale. The project default is configured in Godot's **Project Settings → Localization** tab.
Do not hardcode any visible text outside of `.dialogue` files. All player-facing strings — including item pickup messages, battle narration, and UI prompts — must live in a dialogue file so they are covered by the locale system.
> **Not yet implemented:** locale resolution in `DialogueAction`. Currently loads the resource directly.
> **Implemented:** `DialogueAction._loadLocaleResource(basePath)` resolves the active locale via `TranslationServer.get_locale().left(2)`, falling back to `en` if the locale file is missing.
---