Improved UI textbox
This commit is contained in:
+44
-22
@@ -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.
|
- 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.
|
- **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
|
### 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.
|
…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
|
### 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.
|
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)
|
### 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.
|
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
|
### Advancement modes
|
||||||
|
|
||||||
@@ -151,12 +153,12 @@ Each dialogue sequence uses one of two advancement modes:
|
|||||||
|
|
||||||
| Mode | Behaviour |
|
| Mode | Behaviour |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `PLAYER` (default) | Player presses Interact to advance to the next page or line once reveal is complete. |
|
| `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]` override is supported for lines that need a specific timing. Player input is ignored. |
|
| `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
|
### 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.
|
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
|
### 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
|
## 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.
|
[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
|
```gdscript
|
||||||
# Add a blocking dialogue step to any Cutscene
|
# Add a dialogue step to any Cutscene
|
||||||
cutscene.addCallable(DialogueAction.getDialogueCallable(
|
cutscene.addCallable(DialogueAction.getDialogueCallable(
|
||||||
load("res://dialogue/npc/test.dialogue"),
|
"res://dialogue/npc/guard", # base path — no locale suffix, no extension
|
||||||
"start", # section to begin from
|
"start", # section to begin from
|
||||||
[entity], # extra_game_states exposed to {{variables}} in the file
|
[entity], # extra_game_states exposed to {{variables}} in the file
|
||||||
true # blocking — pauses player movement
|
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 |
|
| 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"`) |
|
| `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 |
|
| File | Used by |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `dialogue/npc/test.dialogue` | TestMap NPC (NotPlayer) |
|
| `dialogue/npc/test.en.dialogue` | TestMap NPC (NotPlayer / "Stranger") |
|
||||||
| `dialogue/item/pickup.dialogue` | `ItemAction` — item pickup text with `{{item_name}}` and `{{quantity}}` |
|
| `dialogue/item/pickup.en.dialogue` | `ItemAction` — item pickup text with `{{item_name}}` and `{{quantity}}` |
|
||||||
| `dialogue/battle/narration.dialogue` | `BattleCutsceneAction` — move announcements, victory/defeat |
|
| `dialogue/battle/narration.en.dialogue` | `BattleCutsceneAction` — move announcements, victory/defeat |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -292,11 +312,13 @@ dialogue/npc/test.en.dialogue
|
|||||||
dialogue/npc/test.ja.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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
class_name BattleCutsceneAction
|
class_name BattleCutsceneAction
|
||||||
const DialogueAction = preload("res://cutscene/dialogue/DialogueAction.gd")
|
|
||||||
|
|
||||||
static var NARRATION:DialogueResource = preload("res://dialogue/battle/narration.dialogue")
|
const _NARRATION_BASE:String = "res://dialogue/battle/narration"
|
||||||
|
|
||||||
# State object passed as extra_game_states so {{variable}} tokens resolve in the dialogue file.
|
# State object passed as extra_game_states so {{variable}} tokens resolve in the dialogue file.
|
||||||
class BattleNarrationState:
|
class BattleNarrationState:
|
||||||
@@ -29,7 +28,7 @@ static func battleDecisionCallable(params:Dictionary) -> int:
|
|||||||
|
|
||||||
var cutscene:Cutscene = params['cutscene']
|
var cutscene:Cutscene = params['cutscene']
|
||||||
cutscene.addCallable(
|
cutscene.addCallable(
|
||||||
DialogueAction.getDialogueCallable(NARRATION, 'move_perform', [state]).merged(
|
DialogueAction.getDialogueCallable(_NARRATION_BASE, 'move_perform', [state], DialogueAction.DialogueMode.NARRATION).merged(
|
||||||
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
|
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -58,7 +57,7 @@ static func playerDecisionCallable(params:Dictionary) -> int:
|
|||||||
|
|
||||||
if allPlayersDead:
|
if allPlayersDead:
|
||||||
params['cutscene'].addCallable(
|
params['cutscene'].addCallable(
|
||||||
DialogueAction.getDialogueCallable(NARRATION, 'battle_defeat').merged(
|
DialogueAction.getDialogueCallable(_NARRATION_BASE, 'battle_defeat', [], DialogueAction.DialogueMode.NARRATION).merged(
|
||||||
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
|
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -66,7 +65,7 @@ static func playerDecisionCallable(params:Dictionary) -> int:
|
|||||||
|
|
||||||
if allEnemiesDead:
|
if allEnemiesDead:
|
||||||
params['cutscene'].addCallable(
|
params['cutscene'].addCallable(
|
||||||
DialogueAction.getDialogueCallable(NARRATION, 'battle_victory').merged(
|
DialogueAction.getDialogueCallable(_NARRATION_BASE, 'battle_victory', [], DialogueAction.DialogueMode.NARRATION).merged(
|
||||||
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
|
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,55 +1,73 @@
|
|||||||
class_name DialogueAction
|
class_name DialogueAction
|
||||||
|
|
||||||
# Runs a .dialogue file through the VNTextbox and returns CUTSCENE_CONTINUE when
|
const _TextboxGd = preload("res://ui/component/DialogueTextbox.gd")
|
||||||
# the last line is dismissed. Mutations in the .dialogue file are executed
|
const _ChoiceBoxGd = preload("res://ui/component/DialogueChoiceBox.gd")
|
||||||
# automatically by DialogueManager before the line is returned.
|
|
||||||
#
|
enum DialogueMode {
|
||||||
# extra_game_states: additional objects/dicts whose properties and methods are
|
CONVERSATION, # blocks movement, player advances
|
||||||
# accessible inside the .dialogue file (alongside all autoloads).
|
NARRATION, # non-blocking, player advances
|
||||||
|
AMBIENT, # non-blocking, timed auto-advance
|
||||||
|
}
|
||||||
|
|
||||||
static func dialogueCallable(params:Dictionary) -> int:
|
static func dialogueCallable(params:Dictionary) -> int:
|
||||||
assert(params.has('resource'))
|
assert(params.has('basePath'))
|
||||||
var resource:DialogueResource = params['resource']
|
var basePath:String = params['basePath']
|
||||||
var title:String = params.get('title', 'start')
|
var title:String = params.get('title', 'start')
|
||||||
var extraStates:Array = params.get('extraStates', [])
|
var extraStates:Array = params.get('extraStates', [])
|
||||||
var characterTargets:Dictionary = params.get('characterTargets', {})
|
var mode:DialogueMode = params.get('mode', DialogueMode.CONVERSATION)
|
||||||
|
|
||||||
|
var resource:DialogueResource = _loadLocaleResource(basePath)
|
||||||
|
assert(resource != null, "DialogueAction: could not load resource for path: " + basePath)
|
||||||
|
|
||||||
|
if mode == DialogueMode.CONVERSATION:
|
||||||
|
UI.activeConversation = true
|
||||||
|
|
||||||
|
var advancementMode:int = (
|
||||||
|
_TextboxGd.AdvancementMode.TIMED
|
||||||
|
if mode == DialogueMode.AMBIENT
|
||||||
|
else _TextboxGd.AdvancementMode.PLAYER
|
||||||
|
)
|
||||||
|
|
||||||
UI.dialogueActive = true
|
|
||||||
var line:DialogueLine = await DialogueManager.get_next_dialogue_line(resource, title, extraStates)
|
var line:DialogueLine = await DialogueManager.get_next_dialogue_line(resource, title, extraStates)
|
||||||
while line != null:
|
while line != null:
|
||||||
var target:Node3D = _getLineTarget(line.character, characterTargets)
|
var entity:Entity = OVERWORLD.getEntityByDialogueName(line.character)
|
||||||
var text:String = line.text
|
|
||||||
|
|
||||||
if line.responses.size() > 0:
|
var textbox = _TextboxGd.SCENE.instantiate()
|
||||||
# Show text then auto-pick the first allowed response.
|
UI.chatBoxContainer.add_child(textbox)
|
||||||
# Replace this block with a real response UI when branching dialogue is needed.
|
textbox.setup(line, entity, advancementMode)
|
||||||
await UI.MAIN_CHATBOX.setTextAndWait(text, target)
|
await textbox.dismissed
|
||||||
var nextId:String = ""
|
|
||||||
for response:DialogueResponse in line.responses:
|
var allowedResponses:Array = line.responses.filter(func(r): return r.is_allowed)
|
||||||
if response.is_allowed:
|
if allowedResponses.size() > 0:
|
||||||
nextId = response.next_id
|
var playerEntity:Entity = OVERWORLD.getPlayerEntity()
|
||||||
break
|
var choiceBox = _ChoiceBoxGd.SCENE.instantiate()
|
||||||
line = await DialogueManager.get_next_dialogue_line(resource, nextId, extraStates)
|
UI.chatBoxContainer.add_child(choiceBox)
|
||||||
|
choiceBox.setup(line.responses, playerEntity)
|
||||||
|
var chosen:DialogueResponse = await choiceBox.chosen
|
||||||
|
line = await DialogueManager.get_next_dialogue_line(resource, chosen.next_id, extraStates)
|
||||||
else:
|
else:
|
||||||
await UI.MAIN_CHATBOX.setTextAndWait(text, target)
|
|
||||||
line = await DialogueManager.get_next_dialogue_line(resource, line.next_id, extraStates)
|
line = await DialogueManager.get_next_dialogue_line(resource, line.next_id, extraStates)
|
||||||
|
|
||||||
UI.dialogueActive = false
|
if mode == DialogueMode.CONVERSATION:
|
||||||
|
UI.activeConversation = false
|
||||||
|
|
||||||
return Cutscene.CUTSCENE_CONTINUE
|
return Cutscene.CUTSCENE_CONTINUE
|
||||||
|
|
||||||
# "player" (case-insensitive) maps to the player entity; any other named
|
static func _loadLocaleResource(basePath:String) -> DialogueResource:
|
||||||
# character maps to the npc entity; empty character (narrator) uses no target.
|
var lang:String = TranslationServer.get_locale().left(2)
|
||||||
static func _getLineTarget(character:String, characterTargets:Dictionary) -> Node3D:
|
var localePath:String = basePath + "." + lang + ".dialogue"
|
||||||
if characterTargets.is_empty() or character == "":
|
if ResourceLoader.exists(localePath):
|
||||||
|
return load(localePath)
|
||||||
|
var fallback:String = basePath + ".en.dialogue"
|
||||||
|
if ResourceLoader.exists(fallback):
|
||||||
|
return load(fallback)
|
||||||
return null
|
return null
|
||||||
if character.to_lower() == "player":
|
|
||||||
return characterTargets.get("player", null)
|
|
||||||
return characterTargets.get("npc", null)
|
|
||||||
|
|
||||||
static func getDialogueCallable(resource:DialogueResource, title:String = 'start', extraStates:Array = [], characterTargets:Dictionary = {}) -> Dictionary:
|
static func getDialogueCallable(basePath:String, title:String = "start", extraStates:Array = [], mode:DialogueMode = DialogueMode.CONVERSATION) -> Dictionary:
|
||||||
return {
|
return {
|
||||||
'function': dialogueCallable,
|
"function": dialogueCallable,
|
||||||
'resource': resource,
|
"basePath": basePath,
|
||||||
'title': title,
|
"title": title,
|
||||||
'extraStates': extraStates,
|
"extraStates": extraStates,
|
||||||
'characterTargets': characterTargets,
|
"mode": mode,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
class_name ItemAction
|
class_name ItemAction
|
||||||
const DialogueAction = preload("res://cutscene/dialogue/DialogueAction.gd")
|
|
||||||
|
|
||||||
# Passed as extra_game_states so {{item_name}} and {{quantity}} resolve in the .dialogue file.
|
|
||||||
class ItemDialogueState:
|
class ItemDialogueState:
|
||||||
var item_name:String
|
var item_name:String
|
||||||
var quantity:int
|
var quantity:int
|
||||||
func _init(name:String, qty:int) -> void:
|
func _init(itemName:String, qty:int) -> void:
|
||||||
item_name = name
|
item_name = itemName
|
||||||
quantity = qty
|
quantity = qty
|
||||||
|
|
||||||
static var PICKUP_DIALOGUE:DialogueResource = preload("res://dialogue/item/pickup.dialogue")
|
|
||||||
|
|
||||||
static func itemGetCallable(params:Dictionary) -> int:
|
static func itemGetCallable(params:Dictionary) -> int:
|
||||||
assert(params.has('stack'))
|
assert(params.has('stack'))
|
||||||
var stack:ItemStack = params['stack']
|
var stack:ItemStack = params['stack']
|
||||||
PARTY.BACKPACK.addStack(stack)
|
PARTY.BACKPACK.addStack(stack)
|
||||||
|
|
||||||
var state = ItemDialogueState.new(Item.getItemName(stack.item), stack.quantity)
|
var state = ItemDialogueState.new(Item.getItemName(stack.item), stack.quantity)
|
||||||
var dialogueParams:Dictionary = DialogueAction.getDialogueCallable(PICKUP_DIALOGUE, 'start', [state])
|
var dialogueParams:Dictionary = DialogueAction.getDialogueCallable(
|
||||||
|
"res://dialogue/item/pickup",
|
||||||
|
"start",
|
||||||
|
[state],
|
||||||
|
DialogueAction.DialogueMode.CONVERSATION
|
||||||
|
)
|
||||||
dialogueParams['position'] = Cutscene.CUTSCENE_ADD_NEXT
|
dialogueParams['position'] = Cutscene.CUTSCENE_ADD_NEXT
|
||||||
params['cutscene'].addCallable(dialogueParams)
|
params['cutscene'].addCallable(dialogueParams)
|
||||||
return Cutscene.CUTSCENE_CONTINUE
|
return Cutscene.CUTSCENE_CONTINUE
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="dialogue_manager"
|
|
||||||
importer_version=15
|
|
||||||
type="Resource"
|
|
||||||
uid="uid://b0xspt5l72ta4"
|
|
||||||
path="res://.godot/imported/narration.dialogue-d3cec8f2ca7d5fcccf22c774ea16000c.tres"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://dialogue/battle/narration.dialogue"
|
|
||||||
dest_files=["res://.godot/imported/narration.dialogue-d3cec8f2ca7d5fcccf22c774ea16000c.tres"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
defaults=true
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="dialogue_manager"
|
||||||
|
importer_version=15
|
||||||
|
type="Resource"
|
||||||
|
uid="uid://c4ik5l43fllwe"
|
||||||
|
path="res://.godot/imported/narration.en.dialogue-306b824321c5ffb5e2681c2a3febbbf1.tres"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://dialogue/battle/narration.en.dialogue"
|
||||||
|
dest_files=["res://.godot/imported/narration.en.dialogue-306b824321c5ffb5e2681c2a3febbbf1.tres"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
defaults=true
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="dialogue_manager"
|
|
||||||
importer_version=15
|
|
||||||
type="Resource"
|
|
||||||
uid="uid://b1xscm8cjvdwa"
|
|
||||||
path="res://.godot/imported/pickup.dialogue-002022bf79323195869be8ebaf81f1dd.tres"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://dialogue/item/pickup.dialogue"
|
|
||||||
dest_files=["res://.godot/imported/pickup.dialogue-002022bf79323195869be8ebaf81f1dd.tres"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
defaults=true
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="dialogue_manager"
|
||||||
|
importer_version=15
|
||||||
|
type="Resource"
|
||||||
|
uid="uid://mldhv5ofaxmf"
|
||||||
|
path="res://.godot/imported/pickup.en.dialogue-9582a3392a037e5325ef2829f22ff4cb.tres"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://dialogue/item/pickup.en.dialogue"
|
||||||
|
dest_files=["res://.godot/imported/pickup.en.dialogue-9582a3392a037e5325ef2829f22ff4cb.tres"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
defaults=true
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="dialogue_manager"
|
|
||||||
importer_version=15
|
|
||||||
type="Resource"
|
|
||||||
uid="uid://b7hdnwp46h3hi"
|
|
||||||
path="res://.godot/imported/test.dialogue-3675c9be06c1457d57c9a2cca7088875.tres"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://dialogue/npc/test.dialogue"
|
|
||||||
dest_files=["res://.godot/imported/test.dialogue-3675c9be06c1457d57c9a2cca7088875.tres"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
defaults=true
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="dialogue_manager"
|
||||||
|
importer_version=15
|
||||||
|
type="Resource"
|
||||||
|
uid="uid://bh0d47hd3edu7"
|
||||||
|
path="res://.godot/imported/test.en.dialogue-5bcd16c222052c4220f719dbcbf91fa5.tres"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://dialogue/npc/test.en.dialogue"
|
||||||
|
dest_files=["res://.godot/imported/test.en.dialogue-5bcd16c222052c4220f719dbcbf91fa5.tres"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
defaults=true
|
||||||
@@ -7,6 +7,25 @@ var hasFadedOut:bool = false
|
|||||||
var playerDestinationNodeName:String
|
var playerDestinationNodeName:String
|
||||||
var newMapLoaded:bool = false
|
var newMapLoaded:bool = false
|
||||||
|
|
||||||
|
var _dialogueEntities:Dictionary = {}
|
||||||
|
var _playerEntity:Entity = null
|
||||||
|
|
||||||
|
func registerDialogueEntity(entity:Entity) -> void:
|
||||||
|
_dialogueEntities[entity.dialogueName.to_lower()] = entity
|
||||||
|
if entity.movementType == Entity.MovementType.PLAYER:
|
||||||
|
_playerEntity = entity
|
||||||
|
|
||||||
|
func unregisterDialogueEntity(entity:Entity) -> void:
|
||||||
|
_dialogueEntities.erase(entity.dialogueName.to_lower())
|
||||||
|
if _playerEntity == entity:
|
||||||
|
_playerEntity = null
|
||||||
|
|
||||||
|
func getEntityByDialogueName(dialogueName:String) -> Entity:
|
||||||
|
return _dialogueEntities.get(dialogueName.to_lower(), null)
|
||||||
|
|
||||||
|
func getPlayerEntity() -> Entity:
|
||||||
|
return _playerEntity
|
||||||
|
|
||||||
func isMapChanging() -> bool:
|
func isMapChanging() -> bool:
|
||||||
return newMapPath != ""
|
return newMapPath != ""
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ enum InteractType {
|
|||||||
ONE_TIME_ITEM,
|
ONE_TIME_ITEM,
|
||||||
CUTSCENE,
|
CUTSCENE,
|
||||||
BATTLE_TEST,
|
BATTLE_TEST,
|
||||||
CHATBOX,
|
|
||||||
PROXIMITY_CHATBOX,
|
PROXIMITY_CHATBOX,
|
||||||
};
|
}
|
||||||
|
|
||||||
@export_category("Identification")
|
@export_category("Identification")
|
||||||
@export var entityId:String = UUID.uuidv4()
|
@export var entityId:String = UUID.uuidv4()
|
||||||
@@ -24,19 +23,30 @@ enum InteractType {
|
|||||||
var button := func():
|
var button := func():
|
||||||
entityId = UUID.uuidv4()
|
entityId = UUID.uuidv4()
|
||||||
|
|
||||||
# Movement settings
|
@export_category("Dialogue")
|
||||||
|
@export var dialogueName:String = ""
|
||||||
|
@export var displayName:String = ""
|
||||||
|
|
||||||
@export_category("Movement")
|
@export_category("Movement")
|
||||||
@export var movementType:MovementType = MovementType.NONE
|
@export var movementType:MovementType = MovementType.NONE
|
||||||
|
|
||||||
# Interaction settings
|
|
||||||
@export_category("Interactions")
|
@export_category("Interactions")
|
||||||
@export var interactType:InteractType = InteractType.NONE
|
@export var interactType:InteractType = InteractType.NONE
|
||||||
@export var dialogueResource:DialogueResource = null
|
@export var dialogueBasePath:String = ""
|
||||||
@export var dialogueTitle:String = "start"
|
@export var dialogueTitle:String = "start"
|
||||||
@export var oneTimeItem:ItemResource = null
|
@export var oneTimeItem:ItemResource = null
|
||||||
@export var cutscene:CutsceneResource = null
|
@export var cutscene:CutsceneResource = null
|
||||||
@export var chatboxMessage:String = ""
|
|
||||||
@export var chatboxDuration:float = 3.0
|
|
||||||
|
|
||||||
# TEST BATTLE
|
|
||||||
@export_category("Test Battle")
|
@export_category("Test Battle")
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return
|
||||||
|
if dialogueName != "":
|
||||||
|
OVERWORLD.registerDialogueEntity(self)
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return
|
||||||
|
if dialogueName != "":
|
||||||
|
OVERWORLD.unregisterDialogueEntity(self)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
class_name EntityInteractableArea extends Area3D
|
class_name EntityInteractableArea extends Area3D
|
||||||
const ItemAction = preload("res://cutscene/item/ItemAction.gd")
|
const ItemAction = preload("res://cutscene/item/ItemAction.gd")
|
||||||
const DialogueAction = preload("res://cutscene/dialogue/DialogueAction.gd")
|
|
||||||
|
|
||||||
@export var entity:Entity
|
@export var entity:Entity
|
||||||
|
|
||||||
@@ -8,44 +7,32 @@ func isInteractable() -> bool:
|
|||||||
if !entity:
|
if !entity:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if entity.interactType == Entity.InteractType.NONE:
|
match entity.interactType:
|
||||||
return false
|
Entity.InteractType.NONE:
|
||||||
|
|
||||||
if entity.interactType == Entity.InteractType.CONVERSATION:
|
|
||||||
return entity.dialogueResource != null
|
|
||||||
|
|
||||||
if entity.interactType == Entity.InteractType.CUTSCENE:
|
|
||||||
if entity.cutscene == null:
|
|
||||||
return false
|
|
||||||
if !entity.cutscene.canRun():
|
|
||||||
return false
|
return false
|
||||||
|
Entity.InteractType.CONVERSATION:
|
||||||
|
return entity.dialogueBasePath != ""
|
||||||
|
Entity.InteractType.CUTSCENE:
|
||||||
|
return entity.cutscene != null and entity.cutscene.canRun()
|
||||||
|
Entity.InteractType.ONE_TIME_ITEM:
|
||||||
|
return (
|
||||||
|
entity.oneTimeItem != null
|
||||||
|
and entity.oneTimeItem.quantity > 0
|
||||||
|
and entity.oneTimeItem.item != Item.Id.NULL
|
||||||
|
)
|
||||||
|
Entity.InteractType.BATTLE_TEST:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if entity.interactType == Entity.InteractType.ONE_TIME_ITEM:
|
|
||||||
if entity.oneTimeItem == null:
|
|
||||||
return false
|
|
||||||
if entity.oneTimeItem.quantity <= 0:
|
|
||||||
return false
|
|
||||||
if entity.oneTimeItem.item == Item.Id.NULL:
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
|
|
||||||
if entity.interactType == Entity.InteractType.BATTLE_TEST:
|
|
||||||
return true
|
|
||||||
|
|
||||||
if entity.interactType == Entity.InteractType.CHATBOX:
|
|
||||||
return entity.chatboxMessage != ""
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
func _onConversationInteract(other:Entity) -> void:
|
func _onConversationInteract(_other:Entity) -> void:
|
||||||
assert(entity.dialogueResource != null)
|
assert(entity.dialogueBasePath != "")
|
||||||
var cutscene:Cutscene = Cutscene.new()
|
var cutscene:Cutscene = Cutscene.new()
|
||||||
cutscene.addCallable(DialogueAction.getDialogueCallable(
|
cutscene.addCallable(DialogueAction.getDialogueCallable(
|
||||||
entity.dialogueResource,
|
entity.dialogueBasePath,
|
||||||
entity.dialogueTitle,
|
entity.dialogueTitle,
|
||||||
[entity],
|
[entity],
|
||||||
{"npc": entity, "player": other}
|
DialogueAction.DialogueMode.CONVERSATION
|
||||||
))
|
))
|
||||||
cutscene.start()
|
cutscene.start()
|
||||||
|
|
||||||
@@ -57,24 +44,17 @@ func _onItemInteract(_other:Entity) -> void:
|
|||||||
entity.queue_free()
|
entity.queue_free()
|
||||||
|
|
||||||
func onInteract(other:Entity) -> void:
|
func onInteract(other:Entity) -> void:
|
||||||
if entity.interactType == Entity.InteractType.NONE:
|
|
||||||
return
|
|
||||||
|
|
||||||
match entity.interactType:
|
match entity.interactType:
|
||||||
|
Entity.InteractType.NONE:
|
||||||
|
return
|
||||||
Entity.InteractType.CONVERSATION:
|
Entity.InteractType.CONVERSATION:
|
||||||
_onConversationInteract(other)
|
_onConversationInteract(other)
|
||||||
return
|
|
||||||
|
|
||||||
Entity.InteractType.ONE_TIME_ITEM:
|
Entity.InteractType.ONE_TIME_ITEM:
|
||||||
_onItemInteract(other)
|
_onItemInteract(other)
|
||||||
return
|
|
||||||
|
|
||||||
Entity.InteractType.CUTSCENE:
|
Entity.InteractType.CUTSCENE:
|
||||||
var cutscene:Cutscene = Cutscene.new()
|
var cutscene:Cutscene = Cutscene.new()
|
||||||
entity.cutscene.queue(cutscene)
|
entity.cutscene.queue(cutscene)
|
||||||
cutscene.start()
|
cutscene.start()
|
||||||
return
|
|
||||||
|
|
||||||
Entity.InteractType.BATTLE_TEST:
|
Entity.InteractType.BATTLE_TEST:
|
||||||
var testEnemy = BattleFighter.new({
|
var testEnemy = BattleFighter.new({
|
||||||
'controller': BattleFighter.FighterController.AI
|
'controller': BattleFighter.FighterController.AI
|
||||||
@@ -85,13 +65,3 @@ func onInteract(other:Entity) -> void:
|
|||||||
BATTLE.BattlePosition.LEFT_MIDDLE_FRONT: testEnemy
|
BATTLE.BattlePosition.LEFT_MIDDLE_FRONT: testEnemy
|
||||||
}))
|
}))
|
||||||
cutscene.start()
|
cutscene.start()
|
||||||
return
|
|
||||||
|
|
||||||
Entity.InteractType.CHATBOX:
|
|
||||||
assert(entity.chatboxMessage != "")
|
|
||||||
var chatbox:WorldChatBox = UI.spawnWorldChatBox(entity)
|
|
||||||
chatbox.showAdvanceable(entity.chatboxMessage)
|
|
||||||
return
|
|
||||||
|
|
||||||
_:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func _applyGravity() -> void:
|
|||||||
func _applyPlayerMovement(delta:float):
|
func _applyPlayerMovement(delta:float):
|
||||||
# Interactions, may move
|
# Interactions, may move
|
||||||
if Input.is_action_just_pressed("interact") && interactingArea && interactingArea.hasInteraction():
|
if Input.is_action_just_pressed("interact") && interactingArea && interactingArea.hasInteraction():
|
||||||
if !UI.hasAdvanceableChatBox() && !UI.dialogueActive:
|
if !UI.dialogueActive:
|
||||||
interactingArea.interact()
|
interactingArea.interact()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ func _applyFriction(delta:float) -> void:
|
|||||||
entity.velocity.z *= delta * FRICTION
|
entity.velocity.z *= delta * FRICTION
|
||||||
|
|
||||||
func _canMove() -> bool:
|
func _canMove() -> bool:
|
||||||
if !UI.MAIN_CHATBOX.isClosed:
|
if UI.activeConversation:
|
||||||
return false
|
return false
|
||||||
if UI.GAME_MENU && UI.GAME_MENU.isOpen():
|
if UI.GAME_MENU && UI.GAME_MENU.isOpen():
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -2,31 +2,12 @@ class_name EntityProximityArea extends Area3D
|
|||||||
|
|
||||||
@export var entity:Entity
|
@export var entity:Entity
|
||||||
|
|
||||||
var _triggered:bool = false
|
|
||||||
var _chatbox:WorldChatBox = null
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
body_entered.connect(_onBodyEntered)
|
body_entered.connect(_onBodyEntered)
|
||||||
body_exited.connect(_onBodyExited)
|
body_exited.connect(_onBodyExited)
|
||||||
|
|
||||||
func _onBodyEntered(body:Node3D) -> void:
|
func _onBodyEntered(_body:Node3D) -> void:
|
||||||
if _triggered:
|
pass
|
||||||
return
|
|
||||||
if !(body is Entity):
|
|
||||||
return
|
|
||||||
if (body as Entity).entityId != "player":
|
|
||||||
return
|
|
||||||
_triggered = true
|
|
||||||
assert(entity != null && entity.chatboxMessage != "")
|
|
||||||
if is_instance_valid(_chatbox) and _chatbox.visible:
|
|
||||||
_chatbox.resetTimer(entity.chatboxDuration)
|
|
||||||
else:
|
|
||||||
_chatbox = UI.spawnWorldChatBox(entity)
|
|
||||||
_chatbox.showTimed(entity.chatboxMessage, entity.chatboxDuration)
|
|
||||||
|
|
||||||
func _onBodyExited(body:Node3D) -> void:
|
func _onBodyExited(_body:Node3D) -> void:
|
||||||
if !(body is Entity):
|
pass
|
||||||
return
|
|
||||||
if (body as Entity).entityId != "player":
|
|
||||||
return
|
|
||||||
_triggered = false
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# Assign dialogue resources after the plugin has imported the .dialogue files.
|
|
||||||
# Once the DialogueManager plugin is enabled in the editor, you can assign
|
|
||||||
# dialogueResource directly in the Inspector instead.
|
|
||||||
var npc:Entity = $NotPlayer
|
var npc:Entity = $NotPlayer
|
||||||
if npc:
|
if npc:
|
||||||
npc.dialogueResource = load("res://dialogue/npc/test.dialogue")
|
npc.dialogueBasePath = "res://dialogue/npc/test"
|
||||||
npc.dialogueTitle = "start"
|
npc.dialogueTitle = "start"
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ script = ExtResource("1_6ms5s")
|
|||||||
[node name="NotPlayer" parent="." instance=ExtResource("2_jmygs")]
|
[node name="NotPlayer" parent="." instance=ExtResource("2_jmygs")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00883961, 1.11219, 0.0142021)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00883961, 1.11219, 0.0142021)
|
||||||
entityId = "bcabec96-8d33-4c16-a997-3bb3b0562b33"
|
entityId = "bcabec96-8d33-4c16-a997-3bb3b0562b33"
|
||||||
|
dialogueName = "stranger"
|
||||||
|
displayName = "Stranger"
|
||||||
interactType = 1
|
interactType = 1
|
||||||
|
|
||||||
[node name="NotPlayer4" parent="." instance=ExtResource("2_jmygs")]
|
[node name="NotPlayer4" parent="." instance=ExtResource("2_jmygs")]
|
||||||
@@ -50,13 +52,11 @@ cutscene = SubResource("Resource_tr4a0")
|
|||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 1.11219, -3)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 1.11219, -3)
|
||||||
entityId = "c1a2b3c4-d5e6-7890-abcd-ef1234567891"
|
entityId = "c1a2b3c4-d5e6-7890-abcd-ef1234567891"
|
||||||
interactType = 5
|
interactType = 5
|
||||||
chatboxMessage = "Hey! Press interact again to close this box."
|
|
||||||
|
|
||||||
[node name="ProximityNPC" parent="." instance=ExtResource("2_jmygs")]
|
[node name="ProximityNPC" parent="." instance=ExtResource("2_jmygs")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 1.11219, -4)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 1.11219, -4)
|
||||||
entityId = "c1a2b3c4-d5e6-7890-abcd-ef1234567892"
|
entityId = "c1a2b3c4-d5e6-7890-abcd-ef1234567892"
|
||||||
interactType = 6
|
interactType = 6
|
||||||
chatboxMessage = "Hey, over here!"
|
|
||||||
|
|
||||||
[node name="EntityProximityArea" type="Area3D" parent="ProximityNPC" node_paths=PackedStringArray("entity")]
|
[node name="EntityProximityArea" type="Area3D" parent="ProximityNPC" node_paths=PackedStringArray("entity")]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
@@ -72,6 +72,8 @@ shape = SubResource("SphereShape3D_prox")
|
|||||||
[node name="Player" parent="." instance=ExtResource("2_jmygs")]
|
[node name="Player" parent="." instance=ExtResource("2_jmygs")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.1915, 1.05, 0.125589)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.1915, 1.05, 0.125589)
|
||||||
entityId = "player"
|
entityId = "player"
|
||||||
|
dialogueName = "john"
|
||||||
|
displayName = "John"
|
||||||
movementType = 2
|
movementType = 2
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="." node_paths=PackedStringArray("targetNode")]
|
[node name="Camera3D" type="Camera3D" parent="." node_paths=PackedStringArray("targetNode")]
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ center_camera={
|
|||||||
[internationalization]
|
[internationalization]
|
||||||
|
|
||||||
locale/translations=PackedStringArray("res://locale/en_AU.po")
|
locale/translations=PackedStringArray("res://locale/en_AU.po")
|
||||||
locale/translations_pot_files=PackedStringArray("res://dialogue/battle/narration.dialogue", "res://dialogue/item/pickup.dialogue", "res://dialogue/npc/test.dialogue")
|
locale/translations_pot_files=PackedStringArray("res://dialogue/battle/narration.en.dialogue", "res://dialogue/item/pickup.en.dialogue", "res://dialogue/npc/test.en.dialogue")
|
||||||
locale/language_filter=["ja"]
|
locale/language_filter=["ja"]
|
||||||
locale/country_filter=["JP"]
|
locale/country_filter=["JP"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
class_name RootUI extends Control
|
class_name RootUI extends Control
|
||||||
|
|
||||||
@export var debugMenu:DebugMenu
|
@export var debugMenu:DebugMenu
|
||||||
@export var mainChatBox:ChatBox
|
|
||||||
@export var gameMenu:GameMenu
|
@export var gameMenu:GameMenu
|
||||||
@export var pauseMenu:PauseMenu
|
@export var pauseMenu:PauseMenu
|
||||||
@export var chatBoxContainer:Control
|
@export var chatBoxContainer:Control
|
||||||
|
|||||||
+4
-10
@@ -1,12 +1,11 @@
|
|||||||
[gd_scene load_steps=6 format=3 uid="uid://baos0arpiskbp"]
|
[gd_scene load_steps=5 format=3 uid="uid://baos0arpiskbp"]
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://bkx3l0kckf4a8" path="res://ui/component/VNTextbox.tscn" id="1_1mtk3"]
|
|
||||||
[ext_resource type="Script" uid="uid://dq3qyyayugt5l" path="res://ui/RootUI.gd" id="1_son71"]
|
[ext_resource type="Script" uid="uid://dq3qyyayugt5l" path="res://ui/RootUI.gd" id="1_son71"]
|
||||||
[ext_resource type="PackedScene" uid="uid://c0i5e2dj11d8c" path="res://ui/pause/PauseMenu.tscn" id="2_atyu8"]
|
[ext_resource type="PackedScene" uid="uid://c0i5e2dj11d8c" path="res://ui/pause/PauseMenu.tscn" id="2_atyu8"]
|
||||||
[ext_resource type="PackedScene" uid="uid://b38dr0wkix76t" path="res://ui/debugmenu/DebugMenu.tscn" id="4_u132g"]
|
[ext_resource type="PackedScene" uid="uid://b38dr0wkix76t" path="res://ui/debugmenu/DebugMenu.tscn" id="4_u132g"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bv5r2x9m4k7n1" path="res://ui/gamemenu/GameMenu.tscn" id="5_gmenu"]
|
[ext_resource type="PackedScene" uid="uid://bv5r2x9m4k7n1" path="res://ui/gamemenu/GameMenu.tscn" id="5_gmenu"]
|
||||||
|
|
||||||
[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "mainChatBox", "gameMenu", "pauseMenu", "chatBoxContainer")]
|
[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "gameMenu", "pauseMenu", "chatBoxContainer")]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
@@ -16,10 +15,9 @@ grow_vertical = 2
|
|||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
script = ExtResource("1_son71")
|
script = ExtResource("1_son71")
|
||||||
debugMenu = NodePath("DebugMenu")
|
debugMenu = NodePath("DebugMenu")
|
||||||
mainChatBox = NodePath("ChatBox")
|
|
||||||
gameMenu = NodePath("GameMenu")
|
gameMenu = NodePath("GameMenu")
|
||||||
pauseMenu = NodePath("PauseMenu")
|
pauseMenu = NodePath("PauseMenu")
|
||||||
chatBoxContainer = NodePath("WorldChatBoxContainer")
|
chatBoxContainer = NodePath("ChatBoxContainer")
|
||||||
metadata/_custom_type_script = "uid://dq3qyyayugt5l"
|
metadata/_custom_type_script = "uid://dq3qyyayugt5l"
|
||||||
|
|
||||||
[node name="DebugMenu" parent="." instance=ExtResource("4_u132g")]
|
[node name="DebugMenu" parent="." instance=ExtResource("4_u132g")]
|
||||||
@@ -35,7 +33,7 @@ process_mode = 3
|
|||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
|
|
||||||
[node name="WorldChatBoxContainer" type="Control" parent="."]
|
[node name="ChatBoxContainer" type="Control" parent="."]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
@@ -43,7 +41,3 @@ anchor_bottom = 1.0
|
|||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
||||||
[node name="ChatBox" parent="." instance=ExtResource("1_1mtk3")]
|
|
||||||
visible = false
|
|
||||||
layout_mode = 1
|
|
||||||
|
|||||||
+26
-34
@@ -2,51 +2,43 @@ extends Node
|
|||||||
|
|
||||||
var rootUi:RootUI = null
|
var rootUi:RootUI = null
|
||||||
|
|
||||||
# True for the entire duration of a DialogueAction run, including the frames
|
# True whenever any dialogue resource is being processed by DialogueManager.
|
||||||
# between lines where the textbox is momentarily closed.
|
# Driven by DialogueManager.dialogue_started / dialogue_ended signals.
|
||||||
var dialogueActive:bool = false
|
var dialogueActive:bool = false
|
||||||
|
|
||||||
var _chatBoxes:Array = []
|
# True only during a CONVERSATION-mode sequence. Blocks player movement.
|
||||||
|
var activeConversation:bool = false
|
||||||
|
|
||||||
var DEBUG_MENU:
|
func _ready() -> void:
|
||||||
|
DialogueManager.dialogue_started.connect(_onDialogueStarted)
|
||||||
|
DialogueManager.dialogue_ended.connect(_onDialogueEnded)
|
||||||
|
|
||||||
|
func _onDialogueStarted(_resource:DialogueResource) -> void:
|
||||||
|
dialogueActive = true
|
||||||
|
|
||||||
|
func _onDialogueEnded(_resource:DialogueResource) -> void:
|
||||||
|
dialogueActive = false
|
||||||
|
|
||||||
|
var chatBoxContainer:Control:
|
||||||
get():
|
get():
|
||||||
if rootUi && rootUi.debugMenu:
|
if rootUi:
|
||||||
|
return rootUi.chatBoxContainer
|
||||||
|
return null
|
||||||
|
|
||||||
|
var DEBUG_MENU:DebugMenu:
|
||||||
|
get():
|
||||||
|
if rootUi:
|
||||||
return rootUi.debugMenu
|
return rootUi.debugMenu
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var MAIN_CHATBOX:
|
var GAME_MENU:GameMenu:
|
||||||
get():
|
get():
|
||||||
if rootUi && rootUi.mainChatBox:
|
if rootUi:
|
||||||
return rootUi.mainChatBox
|
|
||||||
return null
|
|
||||||
|
|
||||||
var GAME_MENU:
|
|
||||||
get():
|
|
||||||
if rootUi && rootUi.gameMenu:
|
|
||||||
return rootUi.gameMenu
|
return rootUi.gameMenu
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var PAUSE_MENU:
|
var PAUSE_MENU:PauseMenu:
|
||||||
get():
|
get():
|
||||||
if rootUi && rootUi.pauseMenu:
|
if rootUi:
|
||||||
return rootUi.pauseMenu
|
return rootUi.pauseMenu
|
||||||
return null
|
return null
|
||||||
|
|
||||||
func addChatBox(box:WorldChatBox) -> void:
|
|
||||||
_chatBoxes.append(box)
|
|
||||||
|
|
||||||
func removeChatBox(box:WorldChatBox) -> void:
|
|
||||||
_chatBoxes.erase(box)
|
|
||||||
|
|
||||||
func hasAdvanceableChatBox() -> bool:
|
|
||||||
for box in _chatBoxes:
|
|
||||||
if box.mode == WorldChatBox.Mode.ADVANCEABLE:
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
func spawnWorldChatBox(target:Node3D = null) -> WorldChatBox:
|
|
||||||
assert(rootUi != null)
|
|
||||||
var chatbox:WorldChatBox = WorldChatBox.SCENE.instantiate()
|
|
||||||
rootUi.chatBoxContainer.add_child(chatbox)
|
|
||||||
chatbox.worldTarget = target
|
|
||||||
return chatbox
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
class_name DialogueChoiceBox extends PanelContainer
|
||||||
|
|
||||||
|
const SCENE:PackedScene = preload("res://ui/component/DialogueChoiceBox.tscn")
|
||||||
|
const MAX_WIDTH:float = 120.0
|
||||||
|
|
||||||
|
signal chosen(response:DialogueResponse)
|
||||||
|
|
||||||
|
var _responses:Array[DialogueResponse] = []
|
||||||
|
var _entity:Entity = null
|
||||||
|
var _selectedIndex:int = 0
|
||||||
|
var _hasLetGoOfInteract:bool = true
|
||||||
|
|
||||||
|
@onready var _list:VBoxContainer = $VBoxContainer/List
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
size.x = MAX_WIDTH
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
func setup(responses:Array[DialogueResponse], entity:Entity) -> void:
|
||||||
|
_entity = entity
|
||||||
|
_responses = responses.filter(func(r): return r.is_allowed)
|
||||||
|
_selectedIndex = 0
|
||||||
|
_hasLetGoOfInteract = !Input.is_action_pressed("interact")
|
||||||
|
|
||||||
|
for child in _list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
for response in _responses:
|
||||||
|
var label:Label = Label.new()
|
||||||
|
label.text = response.text
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_list.add_child(label)
|
||||||
|
|
||||||
|
size.x = MAX_WIDTH
|
||||||
|
visible = true
|
||||||
|
_updateSelection()
|
||||||
|
|
||||||
|
func _process(_delta:float) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
_updateWorldPosition()
|
||||||
|
_processInput()
|
||||||
|
|
||||||
|
func _processInput() -> void:
|
||||||
|
if Input.is_action_just_released("interact"):
|
||||||
|
_hasLetGoOfInteract = true
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("ui_up") or Input.is_action_just_pressed("move_forward"):
|
||||||
|
_selectedIndex = max(0, _selectedIndex - 1)
|
||||||
|
_updateSelection()
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("ui_down") or Input.is_action_just_pressed("move_back"):
|
||||||
|
_selectedIndex = min(_responses.size() - 1, _selectedIndex + 1)
|
||||||
|
_updateSelection()
|
||||||
|
|
||||||
|
if _hasLetGoOfInteract and Input.is_action_just_pressed("interact"):
|
||||||
|
_confirm()
|
||||||
|
|
||||||
|
func _confirm() -> void:
|
||||||
|
if _responses.is_empty():
|
||||||
|
return
|
||||||
|
var response:DialogueResponse = _responses[_selectedIndex]
|
||||||
|
chosen.emit(response)
|
||||||
|
visible = false
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
func _updateSelection() -> void:
|
||||||
|
var children:Array = _list.get_children()
|
||||||
|
for i in children.size():
|
||||||
|
var label:Label = children[i]
|
||||||
|
if i == _selectedIndex:
|
||||||
|
label.add_theme_color_override("font_color", Color.YELLOW)
|
||||||
|
else:
|
||||||
|
label.remove_theme_color_override("font_color")
|
||||||
|
|
||||||
|
func _updateWorldPosition() -> void:
|
||||||
|
if _entity == null:
|
||||||
|
return
|
||||||
|
var camera:Camera3D = get_viewport().get_camera_3d()
|
||||||
|
if camera == null:
|
||||||
|
return
|
||||||
|
var worldPos:Vector3 = _entity.global_position + Vector3(0, 2.5, 0)
|
||||||
|
var screenPos:Vector2 = camera.unproject_position(worldPos)
|
||||||
|
var viewportSize:Vector2 = get_viewport().get_visible_rect().size
|
||||||
|
position = screenPos - Vector2(size.x * 0.5, size.y)
|
||||||
|
position.x = clamp(position.x, 0.0, viewportSize.x - size.x)
|
||||||
|
position.y = clamp(position.y, 0.0, viewportSize.y - size.y)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cbgsqs5t10l06
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[gd_scene load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Theme" path="res://ui/UI Theme.tres" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://ui/component/DialogueChoiceBox.gd" id="2"]
|
||||||
|
|
||||||
|
[node name="DialogueChoiceBox" type="PanelContainer"]
|
||||||
|
custom_minimum_size = Vector2(120, 0)
|
||||||
|
mouse_filter = 2
|
||||||
|
theme = ExtResource("1")
|
||||||
|
script = ExtResource("2")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 1
|
||||||
|
|
||||||
|
[node name="List" type="VBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 2
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
class_name DialogueTextbox extends PanelContainer
|
||||||
|
|
||||||
|
const SCENE:PackedScene = preload("res://ui/component/DialogueTextbox.tscn")
|
||||||
|
|
||||||
|
enum AdvancementMode { PLAYER, TIMED }
|
||||||
|
|
||||||
|
const LINES_PER_PAGE:int = 4
|
||||||
|
const CHARS_PER_SECOND:float = 20.0
|
||||||
|
const SPEEDUP_MULTIPLIER:float = 8.0
|
||||||
|
const PAUSE_COMMA:float = 0.15
|
||||||
|
const PAUSE_SENTENCE:float = 0.4
|
||||||
|
const PAUSE_ELLIPSIS_DOT:float = 0.3
|
||||||
|
const READING_CHARS_PER_SECOND:float = 16.0
|
||||||
|
const MAX_WIDTH:float = 120.0
|
||||||
|
|
||||||
|
signal dismissed
|
||||||
|
|
||||||
|
var _entity:Entity = null
|
||||||
|
var _parsedText:String = ""
|
||||||
|
var _startLine:int = 0
|
||||||
|
var _linesPerPage:int = 0
|
||||||
|
var _revealTimer:float = 0.0
|
||||||
|
var _pauseTimer:float = 0.0
|
||||||
|
var _autoAdvanceTimer:float = 0.0
|
||||||
|
var _isRevealing:bool = false
|
||||||
|
var _isWaitingForInput:bool = false
|
||||||
|
var _hasLetGoOfInteract:bool = true
|
||||||
|
var _advancementMode:AdvancementMode = AdvancementMode.PLAYER
|
||||||
|
|
||||||
|
@onready var _speakerLabel:Label = $VBoxContainer/SpeakerLabel
|
||||||
|
@onready var _bodyLabel:RichTextLabel = $VBoxContainer/BodyLabel
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
size.x = MAX_WIDTH
|
||||||
|
visible = false
|
||||||
|
var scrollbar = _bodyLabel.get_v_scroll_bar()
|
||||||
|
if scrollbar:
|
||||||
|
scrollbar.modulate.a = 0.0
|
||||||
|
scrollbar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
func setup(line:DialogueLine, entity:Entity, mode:AdvancementMode = AdvancementMode.PLAYER) -> void:
|
||||||
|
_entity = entity
|
||||||
|
_advancementMode = mode
|
||||||
|
_speakerLabel.text = entity.displayName if entity else ""
|
||||||
|
_speakerLabel.visible = _speakerLabel.text != ""
|
||||||
|
_bodyLabel.text = line.text
|
||||||
|
_bodyLabel.visible_characters = 0
|
||||||
|
_bodyLabel.scroll_to_line(0)
|
||||||
|
_startLine = 0
|
||||||
|
_linesPerPage = LINES_PER_PAGE
|
||||||
|
_revealTimer = 0.0
|
||||||
|
_pauseTimer = 0.0
|
||||||
|
_autoAdvanceTimer = 0.0
|
||||||
|
_isWaitingForInput = false
|
||||||
|
_hasLetGoOfInteract = !Input.is_action_pressed("interact")
|
||||||
|
size.x = MAX_WIDTH
|
||||||
|
visible = true
|
||||||
|
_parsedText = _bodyLabel.get_parsed_text()
|
||||||
|
_isRevealing = true
|
||||||
|
|
||||||
|
func _process(delta:float) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
|
||||||
|
_updateWorldPosition()
|
||||||
|
|
||||||
|
if _isRevealing:
|
||||||
|
_processReveal(delta)
|
||||||
|
return
|
||||||
|
|
||||||
|
if _isWaitingForInput:
|
||||||
|
_processAdvanceInput()
|
||||||
|
return
|
||||||
|
|
||||||
|
if _advancementMode == AdvancementMode.TIMED:
|
||||||
|
_processAutoAdvance(delta)
|
||||||
|
|
||||||
|
func _processReveal(delta:float) -> void:
|
||||||
|
if _pauseTimer > 0.0:
|
||||||
|
var speedMult:float = SPEEDUP_MULTIPLIER if Input.is_action_pressed("interact") else 1.0
|
||||||
|
_pauseTimer -= delta * speedMult
|
||||||
|
return
|
||||||
|
|
||||||
|
var speedMult:float = SPEEDUP_MULTIPLIER if Input.is_action_pressed("interact") else 1.0
|
||||||
|
_revealTimer += delta * speedMult
|
||||||
|
|
||||||
|
while _revealTimer >= 1.0 / CHARS_PER_SECOND:
|
||||||
|
_revealTimer -= 1.0 / CHARS_PER_SECOND
|
||||||
|
if not _revealNextChar():
|
||||||
|
return
|
||||||
|
|
||||||
|
func _revealNextChar() -> bool:
|
||||||
|
var totalChars:int = len(_parsedText)
|
||||||
|
if _bodyLabel.visible_characters >= totalChars:
|
||||||
|
_onRevealComplete()
|
||||||
|
return false
|
||||||
|
|
||||||
|
_bodyLabel.visible_characters += 1
|
||||||
|
var idx:int = _bodyLabel.visible_characters - 1
|
||||||
|
|
||||||
|
# Stop if this character has wrapped onto the next page.
|
||||||
|
if idx > 0:
|
||||||
|
var charLine:int = _bodyLabel.get_character_line(idx)
|
||||||
|
if charLine >= _startLine + _linesPerPage:
|
||||||
|
_bodyLabel.visible_characters -= 1
|
||||||
|
_onPageFull()
|
||||||
|
return false
|
||||||
|
|
||||||
|
if idx < len(_parsedText):
|
||||||
|
_pauseTimer = _getPauseForChar(idx)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _getPauseForChar(idx:int) -> float:
|
||||||
|
var ch:String = _parsedText[idx]
|
||||||
|
if ch == "," or ch == ";":
|
||||||
|
return PAUSE_COMMA
|
||||||
|
if ch == "." or ch == "!" or ch == "?":
|
||||||
|
if ch == ".":
|
||||||
|
var prevDot:bool = idx > 0 and _parsedText[idx - 1] == "."
|
||||||
|
var nextDot:bool = idx < len(_parsedText) - 1 and _parsedText[idx + 1] == "."
|
||||||
|
if prevDot or nextDot:
|
||||||
|
return PAUSE_ELLIPSIS_DOT
|
||||||
|
return PAUSE_SENTENCE
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
func _onPageFull() -> void:
|
||||||
|
_isRevealing = false
|
||||||
|
_isWaitingForInput = true
|
||||||
|
|
||||||
|
func _onRevealComplete() -> void:
|
||||||
|
_isRevealing = false
|
||||||
|
if _advancementMode == AdvancementMode.TIMED:
|
||||||
|
_autoAdvanceTimer = len(_parsedText) / READING_CHARS_PER_SECOND
|
||||||
|
else:
|
||||||
|
_isWaitingForInput = true
|
||||||
|
|
||||||
|
func _processAdvanceInput() -> void:
|
||||||
|
if Input.is_action_just_released("interact"):
|
||||||
|
_hasLetGoOfInteract = true
|
||||||
|
|
||||||
|
if not _hasLetGoOfInteract:
|
||||||
|
return
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("interact"):
|
||||||
|
_advance()
|
||||||
|
|
||||||
|
func _processAutoAdvance(delta:float) -> void:
|
||||||
|
_autoAdvanceTimer -= delta
|
||||||
|
if _autoAdvanceTimer <= 0.0:
|
||||||
|
_advance()
|
||||||
|
|
||||||
|
func _advance() -> void:
|
||||||
|
var hasMorePages:bool = _startLine + _linesPerPage < _bodyLabel.get_line_count()
|
||||||
|
if hasMorePages:
|
||||||
|
_startLine += _linesPerPage
|
||||||
|
_bodyLabel.scroll_to_line(_startLine)
|
||||||
|
_isWaitingForInput = false
|
||||||
|
_revealTimer = 0.0
|
||||||
|
_pauseTimer = 0.0
|
||||||
|
_isRevealing = true
|
||||||
|
else:
|
||||||
|
dismissed.emit()
|
||||||
|
visible = false
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
func _updateWorldPosition() -> void:
|
||||||
|
if _entity == null:
|
||||||
|
return
|
||||||
|
var camera:Camera3D = get_viewport().get_camera_3d()
|
||||||
|
if camera == null:
|
||||||
|
return
|
||||||
|
var worldPos:Vector3 = _entity.global_position + Vector3(0, 2.5, 0)
|
||||||
|
var screenPos:Vector2 = camera.unproject_position(worldPos)
|
||||||
|
var viewportSize:Vector2 = get_viewport().get_visible_rect().size
|
||||||
|
position = screenPos - size * 0.5
|
||||||
|
position.x = clamp(position.x, 0.0, viewportSize.x - size.x)
|
||||||
|
position.y = clamp(position.y, 0.0, viewportSize.y - size.y)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://drivjdgk70cqq
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[gd_scene load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Theme" path="res://ui/UI Theme.tres" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://ui/component/DialogueTextbox.gd" id="2"]
|
||||||
|
|
||||||
|
[node name="DialogueTextbox" type="PanelContainer"]
|
||||||
|
custom_minimum_size = Vector2(120, 70)
|
||||||
|
mouse_filter = 2
|
||||||
|
theme = ExtResource("1")
|
||||||
|
script = ExtResource("2")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 1
|
||||||
|
|
||||||
|
[node name="SpeakerLabel" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = ""
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BodyLabel" type="RichTextLabel" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 36)
|
||||||
|
size_flags_vertical = 3
|
||||||
|
bbcode_enabled = true
|
||||||
|
scroll_active = true
|
||||||
|
autowrap_mode = 3
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
class_name ChatBox extends PanelContainer
|
|
||||||
|
|
||||||
const VN_REVEAL_TIME = 0.01
|
|
||||||
const _DEFAULT_ANCHOR_TOP = 1.0
|
|
||||||
const _DEFAULT_ANCHOR_RIGHT = 1.0
|
|
||||||
const _DEFAULT_ANCHOR_BOTTOM = 1.0
|
|
||||||
const _DEFAULT_OFFSET_TOP = -58.0
|
|
||||||
|
|
||||||
var label:AdvancedRichText;
|
|
||||||
var parsedOutText = ""
|
|
||||||
var revealTimer:float = 0;
|
|
||||||
|
|
||||||
var lineStarts:Array[int] = [];
|
|
||||||
var newlineIndexes:Array[int] = [];
|
|
||||||
var currentViewScrolled = true;
|
|
||||||
var isSpeedupDown = false;
|
|
||||||
|
|
||||||
var hasLetGoOfInteract:bool = true;
|
|
||||||
var worldTarget:Node3D = null
|
|
||||||
|
|
||||||
var isClosed:bool = false:
|
|
||||||
get():
|
|
||||||
return !self.visible;
|
|
||||||
set(value):
|
|
||||||
self.visible = !value;
|
|
||||||
|
|
||||||
signal chatboxClosing
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
label = $MarginContainer/Label
|
|
||||||
isClosed = true
|
|
||||||
|
|
||||||
func _setWorldLayout(hasTarget:bool) -> void:
|
|
||||||
if hasTarget:
|
|
||||||
anchor_left = 0.0
|
|
||||||
anchor_top = 0.0
|
|
||||||
anchor_right = 0.0
|
|
||||||
anchor_bottom = 0.0
|
|
||||||
offset_left = 0.0
|
|
||||||
offset_top = 0.0
|
|
||||||
offset_right = 0.0
|
|
||||||
offset_bottom = 0.0
|
|
||||||
custom_minimum_size = Vector2(90, 0)
|
|
||||||
else:
|
|
||||||
if label != null:
|
|
||||||
label.fit_content = false
|
|
||||||
anchor_left = 0.0
|
|
||||||
anchor_top = _DEFAULT_ANCHOR_TOP
|
|
||||||
anchor_right = _DEFAULT_ANCHOR_RIGHT
|
|
||||||
anchor_bottom = _DEFAULT_ANCHOR_BOTTOM
|
|
||||||
offset_left = 0.0
|
|
||||||
offset_top = _DEFAULT_OFFSET_TOP
|
|
||||||
offset_right = 0.0
|
|
||||||
offset_bottom = 0.0
|
|
||||||
custom_minimum_size = Vector2.ZERO
|
|
||||||
|
|
||||||
func _updateWorldPosition() -> void:
|
|
||||||
if worldTarget == null:
|
|
||||||
return
|
|
||||||
var camera:Camera3D = get_viewport().get_camera_3d()
|
|
||||||
if camera == null:
|
|
||||||
return
|
|
||||||
var screenPos:Vector2 = camera.unproject_position(worldTarget.global_position + Vector3(0, 2.5, 0))
|
|
||||||
var viewportSize:Vector2 = get_viewport().get_visible_rect().size
|
|
||||||
position = screenPos - size * 0.5
|
|
||||||
position.x = clamp(position.x, 0.0, viewportSize.x - size.x)
|
|
||||||
position.y = clamp(position.y, 0.0, viewportSize.y - size.y)
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
if isClosed:
|
|
||||||
return
|
|
||||||
|
|
||||||
_updateWorldPosition()
|
|
||||||
|
|
||||||
# _recalcText always resets fit_content; keep it on in world-space mode so
|
|
||||||
# the PanelContainer auto-sizes to the full speech-bubble content.
|
|
||||||
if worldTarget != null and !label.fit_content:
|
|
||||||
label.fit_content = true
|
|
||||||
|
|
||||||
if label.getFinalText() == "":
|
|
||||||
isClosed = true
|
|
||||||
return
|
|
||||||
|
|
||||||
if Input.is_action_just_released("interact") and !hasLetGoOfInteract:
|
|
||||||
hasLetGoOfInteract = true
|
|
||||||
return
|
|
||||||
|
|
||||||
# World-space (speech-bubble) mode: all text is shown immediately with no
|
|
||||||
# typing reveal. One press advances to the next page; release closes the last.
|
|
||||||
if worldTarget != null:
|
|
||||||
if (label.maxLines + label.startLine) < label.getTotalLineCount():
|
|
||||||
if Input.is_action_just_pressed("interact") and hasLetGoOfInteract:
|
|
||||||
label.startLine += label.maxLines
|
|
||||||
label.fit_content = true
|
|
||||||
else:
|
|
||||||
if Input.is_action_just_released("interact") and hasLetGoOfInteract:
|
|
||||||
chatboxClosing.emit()
|
|
||||||
isClosed = true
|
|
||||||
return
|
|
||||||
|
|
||||||
# Bottom-bar mode: typing reveal with paging.
|
|
||||||
if label.visible_characters >= label.getCharactersDisplayedCount():
|
|
||||||
if (label.maxLines + label.startLine) < label.getTotalLineCount():
|
|
||||||
if Input.is_action_just_pressed("interact") and hasLetGoOfInteract:
|
|
||||||
label.startLine += label.maxLines
|
|
||||||
label.visible_characters = 0
|
|
||||||
currentViewScrolled = false
|
|
||||||
return
|
|
||||||
currentViewScrolled = true
|
|
||||||
else:
|
|
||||||
if Input.is_action_just_released("interact") and hasLetGoOfInteract:
|
|
||||||
chatboxClosing.emit()
|
|
||||||
isClosed = true
|
|
||||||
currentViewScrolled = true
|
|
||||||
return
|
|
||||||
|
|
||||||
if Input.is_action_just_pressed("interact") and hasLetGoOfInteract:
|
|
||||||
isSpeedupDown = true
|
|
||||||
elif Input.is_action_just_released("interact") and hasLetGoOfInteract:
|
|
||||||
isSpeedupDown = false
|
|
||||||
elif !Input.is_action_pressed("interact") and hasLetGoOfInteract:
|
|
||||||
isSpeedupDown = false
|
|
||||||
|
|
||||||
revealTimer += delta
|
|
||||||
if isSpeedupDown:
|
|
||||||
revealTimer += delta
|
|
||||||
|
|
||||||
if revealTimer > VN_REVEAL_TIME:
|
|
||||||
revealTimer = 0
|
|
||||||
label.visible_characters += 1
|
|
||||||
|
|
||||||
func setText(text:String, target:Node3D = null) -> void:
|
|
||||||
worldTarget = target
|
|
||||||
_setWorldLayout(worldTarget != null)
|
|
||||||
|
|
||||||
revealTimer = 0
|
|
||||||
currentViewScrolled = false
|
|
||||||
label.startLine = 0
|
|
||||||
label.text = text
|
|
||||||
|
|
||||||
if worldTarget != null:
|
|
||||||
# Speech-bubble mode: show all text immediately and auto-size the container.
|
|
||||||
# _recalcText already sets visible_characters = -1; just enable fit_content.
|
|
||||||
label.fit_content = true
|
|
||||||
else:
|
|
||||||
label.visible_characters = 0
|
|
||||||
|
|
||||||
hasLetGoOfInteract = !Input.is_action_pressed("interact")
|
|
||||||
isSpeedupDown = false
|
|
||||||
isClosed = false
|
|
||||||
|
|
||||||
func setTextAndWait(text:String, target:Node3D = null) -> void:
|
|
||||||
self.setText(text, target);
|
|
||||||
await self.chatboxClosing
|
|
||||||
await get_tree().process_frame
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://h8lw23ypcfty
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
[gd_scene load_steps=4 format=3 uid="uid://bkx3l0kckf4a8"]
|
|
||||||
|
|
||||||
[ext_resource type="Theme" uid="uid://dm7ee4aqjr2dl" path="res://ui/UI Theme.tres" id="1_wx4lp"]
|
|
||||||
[ext_resource type="Script" uid="uid://h8lw23ypcfty" path="res://ui/component/VNTextbox.gd" id="2_uo1gm"]
|
|
||||||
[ext_resource type="Script" uid="uid://bjj6upgk1uvxd" path="res://ui/component/advancedrichtext/AdvancedRichText.gd" id="3_m60k3"]
|
|
||||||
|
|
||||||
[node name="ChatBox" type="PanelContainer"]
|
|
||||||
clip_contents = true
|
|
||||||
anchors_preset = 12
|
|
||||||
anchor_top = 1.0
|
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
offset_top = -58.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 0
|
|
||||||
theme = ExtResource("1_wx4lp")
|
|
||||||
script = ExtResource("2_uo1gm")
|
|
||||||
|
|
||||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
|
||||||
clip_contents = true
|
|
||||||
layout_mode = 2
|
|
||||||
theme = ExtResource("1_wx4lp")
|
|
||||||
theme_override_constants/margin_left = 4
|
|
||||||
theme_override_constants/margin_top = 4
|
|
||||||
theme_override_constants/margin_right = 4
|
|
||||||
theme_override_constants/margin_bottom = 4
|
|
||||||
|
|
||||||
[node name="Label" type="RichTextLabel" parent="MarginContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
theme = ExtResource("1_wx4lp")
|
|
||||||
bbcode_enabled = true
|
|
||||||
text = "Hello, I'm an NPC!
|
|
||||||
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
|
|
||||||
Line 1
|
|
||||||
Line 2
|
|
||||||
Line 3
|
|
||||||
Line 4
|
|
||||||
Line 5
|
|
||||||
Line 6
|
|
||||||
Line 7
|
|
||||||
Line 8
|
|
||||||
Line 9
|
|
||||||
Line 10"
|
|
||||||
script = ExtResource("3_m60k3")
|
|
||||||
userText = "Hello, I'm an NPC!
|
|
||||||
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
|
|
||||||
Line 1
|
|
||||||
Line 2
|
|
||||||
Line 3
|
|
||||||
Line 4
|
|
||||||
Line 5
|
|
||||||
Line 6
|
|
||||||
Line 7
|
|
||||||
Line 8
|
|
||||||
Line 9
|
|
||||||
Line 10"
|
|
||||||
_finalText = "Hello, I'm an NPC!
|
|
||||||
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
|
|
||||||
Line 1
|
|
||||||
Line 2"
|
|
||||||
_newLineIndexes = Array[int]([0])
|
|
||||||
_lines = PackedStringArray("Hello, I\'m an NPC!", "This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot\'s built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing", "Line 1", "Line 2", "Line 3", "Line 4", "Line 5", "Line 6", "Line 7", "Line 8", "Line 9", "Line 10")
|
|
||||||
maxLines = 4
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
class_name WorldChatBox extends PanelContainer
|
|
||||||
|
|
||||||
enum Mode { ADVANCEABLE, TIMED }
|
|
||||||
|
|
||||||
const SCENE = preload("res://ui/component/WorldChatBox.tscn")
|
|
||||||
|
|
||||||
signal chatboxClosed
|
|
||||||
|
|
||||||
var mode:Mode = Mode.TIMED
|
|
||||||
var worldTarget:Node3D = null
|
|
||||||
var _timer:float = 0.0
|
|
||||||
var _label:Label
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
_label = $MarginContainer/Label
|
|
||||||
UI.addChatBox(self)
|
|
||||||
visible = false
|
|
||||||
|
|
||||||
func showTimed(text:String, duration:float) -> void:
|
|
||||||
mode = Mode.TIMED
|
|
||||||
_timer = duration
|
|
||||||
_label.text = text
|
|
||||||
visible = true
|
|
||||||
|
|
||||||
func resetTimer(duration:float) -> void:
|
|
||||||
_timer = duration
|
|
||||||
|
|
||||||
func showAdvanceable(text:String) -> void:
|
|
||||||
mode = Mode.ADVANCEABLE
|
|
||||||
_label.text = text
|
|
||||||
visible = true
|
|
||||||
|
|
||||||
func showAndWait(text:String) -> void:
|
|
||||||
showAdvanceable(text)
|
|
||||||
await chatboxClosed
|
|
||||||
|
|
||||||
func close() -> void:
|
|
||||||
UI.removeChatBox(self)
|
|
||||||
chatboxClosed.emit()
|
|
||||||
queue_free()
|
|
||||||
|
|
||||||
func _updateWorldPosition() -> void:
|
|
||||||
if worldTarget == null:
|
|
||||||
return
|
|
||||||
var camera:Camera3D = get_viewport().get_camera_3d()
|
|
||||||
if camera == null:
|
|
||||||
return
|
|
||||||
var screenPos:Vector2 = camera.unproject_position(worldTarget.global_position + Vector3(0, 2.5, 0))
|
|
||||||
var viewportSize:Vector2 = get_viewport().get_visible_rect().size
|
|
||||||
position = screenPos - size * 0.5
|
|
||||||
position.x = clamp(position.x, 0.0, viewportSize.x - size.x)
|
|
||||||
position.y = clamp(position.y, 0.0, viewportSize.y - size.y)
|
|
||||||
|
|
||||||
func _process(delta:float) -> void:
|
|
||||||
if !visible:
|
|
||||||
return
|
|
||||||
_updateWorldPosition()
|
|
||||||
match mode:
|
|
||||||
Mode.TIMED:
|
|
||||||
_timer -= delta
|
|
||||||
if _timer <= 0.0:
|
|
||||||
close()
|
|
||||||
Mode.ADVANCEABLE:
|
|
||||||
if Input.is_action_just_pressed("interact"):
|
|
||||||
close()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://m1keb2pw8bqq
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
[gd_scene load_steps=3 format=3]
|
|
||||||
|
|
||||||
[ext_resource type="Theme" uid="uid://dm7ee4aqjr2dl" path="res://ui/UI Theme.tres" id="1_wx4lp"]
|
|
||||||
[ext_resource type="Script" path="res://ui/component/WorldChatBox.gd" id="2_wcb"]
|
|
||||||
|
|
||||||
[node name="WorldChatBox" type="PanelContainer"]
|
|
||||||
custom_minimum_size = Vector2(90, 0)
|
|
||||||
mouse_filter = 2
|
|
||||||
theme = ExtResource("1_wx4lp")
|
|
||||||
script = ExtResource("2_wcb")
|
|
||||||
|
|
||||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
|
||||||
layout_mode = 2
|
|
||||||
theme_override_constants/margin_left = 5
|
|
||||||
theme_override_constants/margin_top = 3
|
|
||||||
theme_override_constants/margin_right = 5
|
|
||||||
theme_override_constants/margin_bottom = 3
|
|
||||||
|
|
||||||
[node name="Label" type="Label" parent="MarginContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
autowrap_mode = 3
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
@tool
|
|
||||||
class_name AdvancedRichText extends RichTextLabel
|
|
||||||
|
|
||||||
@export_multiline var userText:String = "" # The text the user is asking for
|
|
||||||
@export_multiline var _finalText:String = "" # The final text after processing (translation, wrapping, etc.)
|
|
||||||
|
|
||||||
@export var _newLineIndexes:Array[int] = [] # The indexes of where each line starts in finalText
|
|
||||||
@export var _lines:PackedStringArray = [];
|
|
||||||
|
|
||||||
# Hides the original RichTextLabel text property
|
|
||||||
func _set(property: StringName, value) -> bool:
|
|
||||||
if property == "text":
|
|
||||||
userText = value
|
|
||||||
_recalcText()
|
|
||||||
return true
|
|
||||||
elif property == "richtextlabel_text":
|
|
||||||
text = value
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
func _get(property: StringName):
|
|
||||||
if property == "text":
|
|
||||||
return userText
|
|
||||||
elif property == "richtextlabel_text":
|
|
||||||
return text
|
|
||||||
return null
|
|
||||||
|
|
||||||
@export var translate:bool = true:
|
|
||||||
set(value):
|
|
||||||
translate = value
|
|
||||||
_recalcText()
|
|
||||||
get():
|
|
||||||
return translate
|
|
||||||
|
|
||||||
@export var smartWrap:bool = true:
|
|
||||||
set(value):
|
|
||||||
smartWrap = value
|
|
||||||
_recalcText()
|
|
||||||
get():
|
|
||||||
return smartWrap
|
|
||||||
|
|
||||||
@export var maxLines:int = -1:
|
|
||||||
set(value):
|
|
||||||
maxLines = value
|
|
||||||
_recalcText()
|
|
||||||
get():
|
|
||||||
return maxLines
|
|
||||||
|
|
||||||
@export var startLine:int = 0:
|
|
||||||
set(value):
|
|
||||||
startLine = value
|
|
||||||
_recalcText()
|
|
||||||
get():
|
|
||||||
return startLine
|
|
||||||
|
|
||||||
# Returns count of characters that can be displayed, assuming visible_chars = -1
|
|
||||||
func getCharactersDisplayedCount() -> int:
|
|
||||||
# Count characters
|
|
||||||
var count = 0
|
|
||||||
var lineCount = min(startLine + maxLines, _lines.size()) - startLine
|
|
||||||
for i in range(startLine, startLine + lineCount):
|
|
||||||
count += _lines[i].length()
|
|
||||||
if lineCount > 1:
|
|
||||||
count += lineCount - 1 # Add newlines
|
|
||||||
return count
|
|
||||||
|
|
||||||
func getFinalText() -> String:
|
|
||||||
return _finalText
|
|
||||||
|
|
||||||
func getTotalLineCount() -> int:
|
|
||||||
return _lines.size()
|
|
||||||
|
|
||||||
func _enter_tree() -> void:
|
|
||||||
self.threaded = false;
|
|
||||||
|
|
||||||
func _recalcText() -> void:
|
|
||||||
_lines.clear()
|
|
||||||
|
|
||||||
if userText.is_empty():
|
|
||||||
self.richtextlabel_text = ""
|
|
||||||
return
|
|
||||||
|
|
||||||
# Translate if needed
|
|
||||||
var textTranslated = userText
|
|
||||||
if self.translate:
|
|
||||||
textTranslated = tr(textTranslated)
|
|
||||||
|
|
||||||
# Replace input bb tags.
|
|
||||||
var regex = RegEx.new()
|
|
||||||
regex.compile(r"\[input action=(.*?)\](.*?)\[/input\]")
|
|
||||||
var inputIconText = textTranslated
|
|
||||||
for match in regex.search_all(textTranslated):
|
|
||||||
var action = match.get_string(1).to_lower()
|
|
||||||
var height:int = get_theme_font_size("normal_font_size")
|
|
||||||
var img_tag = "[img height=%d valign=center,center]res://ui/input/%s.tres[/img]" % [ height, action ]
|
|
||||||
inputIconText = inputIconText.replace(match.get_string(0), img_tag)
|
|
||||||
|
|
||||||
# Perform smart wrapping
|
|
||||||
var wrappedText = inputIconText
|
|
||||||
if smartWrap:
|
|
||||||
var unwrappedText = wrappedText.strip_edges()
|
|
||||||
|
|
||||||
self.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART;
|
|
||||||
self.richtextlabel_text = unwrappedText
|
|
||||||
self.visible_characters = -1;
|
|
||||||
self.fit_content = false;
|
|
||||||
_newLineIndexes = [];
|
|
||||||
|
|
||||||
# Determine where the wrapped newlines are
|
|
||||||
var line = 0;
|
|
||||||
var wasNewLine = false;
|
|
||||||
for i in range(0, self.richtextlabel_text.length()):
|
|
||||||
var tLine = self.get_character_line(i);
|
|
||||||
if tLine == line:
|
|
||||||
wasNewLine = false
|
|
||||||
if self.richtextlabel_text[i] == "\n":
|
|
||||||
wasNewLine = true
|
|
||||||
continue;
|
|
||||||
if !wasNewLine:
|
|
||||||
_newLineIndexes.append(i);
|
|
||||||
line = tLine;
|
|
||||||
|
|
||||||
# Create fake pre-wrapped text.
|
|
||||||
wrappedText = "";
|
|
||||||
for i in range(0, self.richtextlabel_text.length()):
|
|
||||||
if _newLineIndexes.find(i) != -1 and i != 0:
|
|
||||||
wrappedText += "\n";
|
|
||||||
wrappedText += self.richtextlabel_text[i];
|
|
||||||
|
|
||||||
# Handle max and start line(s)
|
|
||||||
var maxText = wrappedText
|
|
||||||
if maxLines > 0:
|
|
||||||
_lines = maxText.split("\n", true);
|
|
||||||
var selectedLines = [];
|
|
||||||
for i in range(startLine, min(startLine + maxLines, _lines.size())):
|
|
||||||
selectedLines.append(_lines[i]);
|
|
||||||
maxText = "\n".join(selectedLines);
|
|
||||||
|
|
||||||
_finalText = maxText
|
|
||||||
self.richtextlabel_text = maxText
|
|
||||||
# print("Updated text")
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bjj6upgk1uvxd
|
|
||||||
@@ -41,7 +41,7 @@ func _unhandled_input(event:InputEvent) -> void:
|
|||||||
if event.is_action_pressed("menu"):
|
if event.is_action_pressed("menu"):
|
||||||
if visible:
|
if visible:
|
||||||
close()
|
close()
|
||||||
elif !UI.dialogueActive && UI.MAIN_CHATBOX.isClosed:
|
elif !UI.dialogueActive:
|
||||||
open()
|
open()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user