Some changes
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# Architecture
|
||||
|
||||
## Singletons (Autoloads)
|
||||
|
||||
Never instantiate these — access only via the global handle.
|
||||
|
||||
| Handle | Role |
|
||||
|---|---|
|
||||
| `SCENE` | Current scene state; `SCENE.setScene(SceneSingleton.SceneType.X)` to switch |
|
||||
| `TRANSITION` | Fade in/out; `TRANSITION.fade(FadeType, duration, color)` |
|
||||
| `BATTLE` | Battle state and fighter map |
|
||||
| `PARTY` | Party members (`PARTY.getFullParty()`) and `PARTY.BACKPACK` inventory |
|
||||
| `OVERWORLD` | Map switching with threaded loading |
|
||||
| `COOKING` | Cooking mini-game lifecycle |
|
||||
| `SAVE` | Persistence (stub) |
|
||||
| `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` |
|
||||
|
||||
## Scene Graph
|
||||
|
||||
```
|
||||
RootScene (Node3D)
|
||||
└─ overworld / battle / cooking / initial ← one shown at a time
|
||||
RootUI (Control, always visible)
|
||||
└─ VNTextbox, DebugMenu
|
||||
```
|
||||
|
||||
`RootScene` listens to `SCENE.sceneChanged` and shows/hides the correct sub-tree.
|
||||
|
||||
## Cutscene / Event Queue
|
||||
|
||||
`Cutscene` is the universal sequencing engine. It holds an `Array[Dictionary]` queue; each entry has a `"function": Callable` plus arbitrary data keys.
|
||||
|
||||
**Return codes from a callable:**
|
||||
- `Cutscene.CUTSCENE_CONTINUE` — advance to next item
|
||||
- `Cutscene.CUTSCENE_END` — stop the cutscene
|
||||
- An integer index — jump to that position
|
||||
|
||||
**Insertion positions:**
|
||||
- `Cutscene.CUTSCENE_ADD_END` — append (default)
|
||||
- `Cutscene.CUTSCENE_ADD_NEXT` — insert immediately after current
|
||||
|
||||
**Callable pattern** — every action class exposes a pair:
|
||||
|
||||
```gdscript
|
||||
# The actual callable (static, takes params:Dictionary, returns int)
|
||||
static func myCallable(params:Dictionary) -> int:
|
||||
...
|
||||
return Cutscene.CUTSCENE_CONTINUE
|
||||
|
||||
# Factory that builds the dictionary for addCallable()
|
||||
static func getMyCallable(arg) -> Dictionary:
|
||||
return { "function": myCallable, "myArg": arg }
|
||||
```
|
||||
|
||||
## Data Registry Pattern
|
||||
|
||||
Static registries (Item, Recipe) follow this pattern:
|
||||
1. `enum Id { NULL, ... }` — typed identifier
|
||||
2. `static var DATA:Array = []` — indexed by Id value
|
||||
3. `static func define(params) -> Dictionary` — called at class load to populate `DATA`
|
||||
4. `static var FOO = define({...})` — registers the entry as a static var
|
||||
5. `static func get*(id) -> *` — typed accessors
|
||||
|
||||
## `_init(params:Dictionary)` Pattern
|
||||
|
||||
Non-Node data classes (`BattleFighter`, `BattleDecision`, `ItemStack`, etc.) use a single `params` dictionary constructor with `.get('key', default)` for optional fields.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Code Style
|
||||
|
||||
## Naming
|
||||
|
||||
| Kind | Convention | Examples |
|
||||
|---|---|---|
|
||||
| Variables & functions | camelCase | `fighterMap`, `startBattle()`, `getFullParty()` |
|
||||
| Classes, enums, enum values | PascalCase | `BattleFighter`, `FighterTeam`, `ALLY` |
|
||||
| Constants & static data | SCREAMING_SNAKE_CASE | `CUTSCENE_CONTINUE`, `ITEM_DATA`, `PARTY_JOHN` |
|
||||
| Private/internal helpers | leading underscore | `_onConversationInteract()`, `_applyGravity()` |
|
||||
|
||||
No trailing underscores. Leading underscore = "don't call this externally."
|
||||
|
||||
## Formatting
|
||||
|
||||
- **2-space indent** — not 4, not tabs
|
||||
- **Type annotations everywhere**: `var health:int`, `func damage(amount:int, crit:bool) -> void:`
|
||||
- **No space** between name and type: `var foo:int` not `var foo : int`
|
||||
- Blank lines between logical sections; comment headers label groups: `# Health`, `# Signals`
|
||||
|
||||
## General Rules
|
||||
|
||||
- `assert()` for invariants and preconditions — prefer over silent failures
|
||||
- `params:Dictionary` for multi-argument constructors/callables; access with `.get('key', default)` or `.has('key')` guards
|
||||
- `match` for enum dispatch; `if/elif` chains for non-exhaustive checks
|
||||
- `continue` / early `return` to flatten nesting instead of deep else-branches
|
||||
- Avoid long inline lambdas — extract named static functions when logic is non-trivial
|
||||
@@ -0,0 +1,89 @@
|
||||
# Dialogue System
|
||||
|
||||
All authored text — NPC conversations, item pickup messages, battle narration — is written in `.dialogue` files and played back via `DialogueManager` (godot_dialogue_manager v3.10.4).
|
||||
|
||||
## Plugin
|
||||
|
||||
- Autoload: `DialogueManager` (registered in `project.godot`)
|
||||
- Plugin must be enabled in Godot → Project Settings → Plugins → Dialogue Manager
|
||||
- `.dialogue` files are imported as `DialogueResource` once the plugin is active
|
||||
- Dialogue files live in `dialogue/` organised by category (`npc/`, `item/`, `battle/`)
|
||||
|
||||
## DialogueAction — the Cutscene bridge
|
||||
|
||||
[cutscene/dialogue/DialogueAction.gd](../../cutscene/dialogue/DialogueAction.gd) is the glue between the `Cutscene` queue and `DialogueManager`. It runs a `.dialogue` file through `VNTextbox` line-by-line and returns `CUTSCENE_CONTINUE` when the last line is dismissed.
|
||||
|
||||
```gdscript
|
||||
# Add a dialogue step to any Cutscene
|
||||
cutscene.addCallable(DialogueAction.getDialogueCallable(
|
||||
load("res://dialogue/npc/test.dialogue"),
|
||||
"start", # title to begin from
|
||||
[entity] # extra_game_states: objects/dicts accessible in the .dialogue file
|
||||
))
|
||||
```
|
||||
|
||||
Movement is blocked automatically while dialogue runs because `VNTextbox` is open (`EntityMovement._canMove()` checks `UI.TEXTBOX.isClosed`).
|
||||
|
||||
## Writing .dialogue files
|
||||
|
||||
```
|
||||
~ title_name # entry point / jump target
|
||||
|
||||
Speaker: Line of dialogue.
|
||||
Another line with no speaker.
|
||||
|
||||
~ another_section
|
||||
Speaker: Variables resolve inline: {{some_property}}.
|
||||
=> END # end this dialogue
|
||||
```
|
||||
|
||||
**Key syntax:**
|
||||
- `~ title` — section anchor; use as the `title` argument to `DialogueAction`
|
||||
- `=> title` — jump to another section; `=> END` ends the dialogue
|
||||
- `{{variable}}` — resolves a property from any autoload or `extra_game_states` object
|
||||
- `do AUTOLOAD.method()` — call a method on any autoload (e.g. `do PARTY.BACKPACK.addStack(stack)`)
|
||||
- `set AUTOLOAD.property = value` — set a property
|
||||
- `- Option text` — response branch (indented lines handle each branch)
|
||||
- `[if condition]` — conditional line or response
|
||||
|
||||
**Mutations fire automatically** before the next dialogue line is returned — you do not handle them manually in GDScript.
|
||||
|
||||
## Passing runtime data to dialogue
|
||||
|
||||
Use `extra_game_states` to expose GDScript objects to `{{variable}}` tokens:
|
||||
|
||||
```gdscript
|
||||
class ItemDialogueState:
|
||||
var item_name:String
|
||||
var quantity:int
|
||||
|
||||
DialogueAction.getDialogueCallable(resource, 'start', [ItemDialogueState.new("Potato", 1)])
|
||||
```
|
||||
|
||||
Then in the `.dialogue` file:
|
||||
```
|
||||
Obtained {{item_name}} x{{quantity}}.
|
||||
```
|
||||
|
||||
## NPC entities
|
||||
|
||||
Set these two exports on an Entity node whose `interactType = CONVERSATION`:
|
||||
|
||||
| Export | Purpose |
|
||||
|---|---|
|
||||
| `dialogueResource:DialogueResource` | The `.dialogue` file to run |
|
||||
| `dialogueTitle:String` | Title (section) to start from (default `"start"`) |
|
||||
|
||||
After first enabling the plugin and reimporting, assign `dialogueResource` directly in the Inspector. Until then, assign it in code (see [TestMap.gd](../../overworld/map/TestMap.gd) for the pattern).
|
||||
|
||||
## Dialogue files
|
||||
|
||||
| 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 |
|
||||
|
||||
## Response branching (current limitation)
|
||||
|
||||
`DialogueAction` auto-selects the **first allowed response** when a line has multiple options. There is no in-game response UI yet. To add one, replace the auto-select block in `DialogueAction.dialogueCallable` with a call to your response menu and `await` its selection signal.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Stubs & Incomplete Systems
|
||||
|
||||
These exist in the codebase but have no real implementation yet. Don't assume they work.
|
||||
|
||||
| File / Symbol | Status |
|
||||
|---|---|
|
||||
| `Save.gd` | No actual serialization — all persistence is a stub |
|
||||
| `Quest.gd` | Empty singleton |
|
||||
| `CutsceneSingleton.gd` | Minimal stub |
|
||||
| `BattleFighter.getAIDecision()` | Always returns `null` |
|
||||
| `BattleDecision.execute()` | Stub |
|
||||
| `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 |
|
||||
@@ -0,0 +1,26 @@
|
||||
# System Conventions
|
||||
|
||||
## Battle
|
||||
|
||||
- Fighters live in `BATTLE.fighterMap: Dictionary[BattlePosition, BattleFighter]`
|
||||
- `BattleFighter` is pure data (no Node); `BattleFighterScene` is the 3D visual
|
||||
- Battle flow is driven by `BattleCutsceneAction.playerDecisionCallable` which loops via `CUTSCENE_ADD_END`
|
||||
- New moves → add static preset in [battle/fighter/BattleMove.gd](../../battle/fighter/BattleMove.gd), add `perform()` logic in [battle/action/BattleMove.gd](../../battle/action/BattleMove.gd)
|
||||
|
||||
## Entities (Overworld)
|
||||
|
||||
- All interactable world objects extend `Entity` (CharacterBody3D)
|
||||
- Interaction type set via `@export var interactType:InteractType`
|
||||
- Interaction routing lives in `EntityInteractableArea.onInteract()` — add new `InteractType` values there
|
||||
- NPC conversation entities: set `interactType = CONVERSATION`, assign `dialogueResource` (a `.dialogue` file) and `dialogueTitle` (section to start from, default `"start"`)
|
||||
|
||||
## Items
|
||||
|
||||
- Register new items in [item/Item.gd](../../item/Item.gd) — add to `Id` enum and call `itemDefine()`
|
||||
- Item ID order in the enum **must match** insertion order into `ITEM_DATA`
|
||||
|
||||
## UI
|
||||
|
||||
- `UI.TEXTBOX.setTextAndWait(text)` — show dialogue and await player dismiss (use `await`)
|
||||
- Movement is blocked automatically when `UI.TEXTBOX` is visible (`EntityMovement._canMove()` checks this)
|
||||
- Menus extend `ClosableMenu` for open/close/toggle + `closed`/`opened` signals
|
||||
@@ -1,129 +1,19 @@
|
||||
# Dawn Godot — Claude Code Guide
|
||||
|
||||
## Project
|
||||
|
||||
Godot 4.4 RPG prototype, GDScript only. Viewport 480×270 (scaled 3× to 1440×810), GL Compatibility renderer.
|
||||
|
||||
---
|
||||
## Reference docs
|
||||
|
||||
## Code Style
|
||||
Detailed reference lives in [.claude/docs/](.claude/docs/):
|
||||
|
||||
### Naming
|
||||
- **camelCase** for variables and functions: `fighterMap`, `startBattle()`, `getFullParty()`
|
||||
- **PascalCase** for class names, enums, and enum values: `BattleFighter`, `FighterTeam`, `ALLY`
|
||||
- **SCREAMING_SNAKE_CASE** for constants and static data: `CUTSCENE_CONTINUE`, `ITEM_DATA`, `PARTY_JOHN`
|
||||
- **No** trailing underscores on private methods — use a leading underscore only for internal helpers that shouldn't be called externally: `_onConversationInteract()`, `_applyGravity()`
|
||||
- [Code Style](.claude/docs/code-style.md) — naming, formatting, general rules
|
||||
- [Architecture](.claude/docs/architecture.md) — singletons, scene graph, cutscene queue, data registry, `_init` pattern
|
||||
- [Systems](.claude/docs/systems.md) — battle, entities, items, UI conventions
|
||||
- [Dialogue](.claude/docs/dialogue.md) — DialogueManager integration, writing .dialogue files, DialogueAction
|
||||
- [Stubs](.claude/docs/stubs.md) — incomplete / placeholder systems to avoid relying on
|
||||
|
||||
### Formatting
|
||||
- 2-space indent (not 4, not tabs)
|
||||
- Type annotations on all variable declarations and function signatures: `var health:int`, `func damage(amount:int, crit:bool) -> void:`
|
||||
- No space between variable name and type: `var foo:int` not `var foo : int`
|
||||
- Blank lines between logical sections within a file; comment headers (`# Health`, `# Signals`) to label groups
|
||||
|
||||
### General Rules
|
||||
- `assert()` for invariants and preconditions — prefer it over silent failures
|
||||
- `params:Dictionary` pattern for multi-argument constructors/callables; access with `.get('key', default)` or `.has('key')` guards
|
||||
- `match` for enum dispatch; `if/elif` chains for non-exhaustive checks
|
||||
- `continue` / early `return` to reduce nesting rather than deep else-branches
|
||||
- Avoid long inline lambdas; extract named static functions where logic is non-trivial
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Singletons (Autoloads)
|
||||
All globally accessible by their handle. Never instantiate these — access via the global name.
|
||||
|
||||
| Handle | Role |
|
||||
|---|---|
|
||||
| `SCENE` | Current scene state; call `SCENE.setScene(SceneSingleton.SceneType.X)` to switch |
|
||||
| `TRANSITION` | Fade in/out; `TRANSITION.fade(FadeType, duration, color)` |
|
||||
| `BATTLE` | Battle state and fighter map |
|
||||
| `PARTY` | Party members (`PARTY.getFullParty()`) and `PARTY.BACKPACK` inventory |
|
||||
| `OVERWORLD` | Map switching with threaded loading |
|
||||
| `COOKING` | Cooking mini-game lifecycle |
|
||||
| `SAVE` | Persistence (stub) |
|
||||
| `QUEST` | Quest management (stub) |
|
||||
| `CUTSCENE` | Cutscene global (stub) |
|
||||
| `UI` | Root UI accessor — `UI.TEXTBOX`, `UI.DEBUG_MENU` |
|
||||
|
||||
### Scene Graph
|
||||
```
|
||||
RootScene (Node3D)
|
||||
└─ overworld / battle / cooking / initial ← one shown at a time
|
||||
RootUI (Control, always visible)
|
||||
└─ VNTextbox, DebugMenu
|
||||
```
|
||||
|
||||
`RootScene` listens to `SCENE.sceneChanged` and shows/hides the appropriate sub-tree.
|
||||
|
||||
### Cutscene / Event Queue
|
||||
`Cutscene` is the universal sequencing engine. It holds an `Array[Dictionary]` queue; each entry has a `"function": Callable` plus arbitrary data keys.
|
||||
|
||||
**Return codes from a callable:**
|
||||
- `Cutscene.CUTSCENE_CONTINUE` — advance to next item
|
||||
- `Cutscene.CUTSCENE_END` — stop the cutscene
|
||||
- An integer index — jump to that position
|
||||
|
||||
**Position constants when adding:**
|
||||
- `Cutscene.CUTSCENE_ADD_END` — append (default)
|
||||
- `Cutscene.CUTSCENE_ADD_NEXT` — insert immediately after current
|
||||
|
||||
**Callable pattern** — every action class exposes a pair:
|
||||
```gdscript
|
||||
# The actual callable (static, takes params:Dictionary, returns int)
|
||||
static func myCallable(params:Dictionary) -> int:
|
||||
...
|
||||
return Cutscene.CUTSCENE_CONTINUE
|
||||
|
||||
# Factory that builds the dictionary for addCallable()
|
||||
static func getMyCallable(arg) -> Dictionary:
|
||||
return { "function": myCallable, "myArg": arg }
|
||||
```
|
||||
|
||||
### Data Registry Pattern
|
||||
Static registries (Item, Recipe) follow this pattern:
|
||||
1. `enum Id { NULL, ... }` — typed identifier
|
||||
2. `static var DATA:Array = []` — indexed by Id value
|
||||
3. `static func define(params) -> Dictionary` — called at class load to populate `DATA`
|
||||
4. `static var FOO = define({...})` — registers the entry as a static var
|
||||
5. `static func get*(id) -> *` — typed accessors
|
||||
|
||||
### `_init(params:Dictionary)` Pattern
|
||||
Non-Node data classes (`BattleFighter`, `BattleDecision`, `ItemStack`, etc.) use a single `params` dictionary constructor with `.get('key', default)` for optional fields.
|
||||
|
||||
---
|
||||
|
||||
## System Conventions
|
||||
|
||||
### Battle
|
||||
- Fighters live in `BATTLE.fighterMap: Dictionary[BattlePosition, BattleFighter]`
|
||||
- `BattleFighter` is pure data (no Node); `BattleFighterScene` is the 3D visual
|
||||
- Battle flow is driven by `BattleCutsceneAction.playerDecisionCallable` which loops via `CUTSCENE_ADD_END`
|
||||
- New moves go in [battle/fighter/BattleMove.gd](battle/fighter/BattleMove.gd) as static presets; add corresponding `perform()` logic in [battle/action/BattleMove.gd](battle/action/BattleMove.gd)
|
||||
|
||||
### Entities (Overworld)
|
||||
- All interactable world objects extend `Entity` (CharacterBody3D)
|
||||
- Interaction type is set via `@export var interactType:InteractType`
|
||||
- Interaction routing lives in `EntityInteractableArea.onInteract()` — add new `InteractType` values there
|
||||
|
||||
### Items
|
||||
- Register new items in [item/Item.gd](item/Item.gd) — add to `Id` enum and call `itemDefine()`
|
||||
- Item ID order in the enum must match insertion order into `ITEM_DATA`
|
||||
|
||||
### UI
|
||||
- `UI.TEXTBOX.setTextAndWait(text)` — show dialogue and await player dismiss (use `await`)
|
||||
- Movement is blocked automatically when `UI.TEXTBOX` is visible (`EntityMovement._canMove()` checks this)
|
||||
- Menus extend `ClosableMenu` for open/close/toggle + `closed`/`opened` signals
|
||||
|
||||
---
|
||||
|
||||
## What's Incomplete / Stub
|
||||
- `Save.gd` — no actual serialization yet
|
||||
- `Quest.gd` — empty singleton
|
||||
- `CutsceneSingleton.gd` — minimal stub
|
||||
- `BattleFighter.getAIDecision()` — always returns `null`
|
||||
- `BattleDecision.execute()` — stub
|
||||
- `BattleItem.perform()` — stub
|
||||
- `CookingScene.tscn` — placeholder UI only
|
||||
- `Pause.gd` — commented-out logic
|
||||
@.claude/docs/code-style.md
|
||||
@.claude/docs/architecture.md
|
||||
@.claude/docs/systems.md
|
||||
@.claude/docs/dialogue.md
|
||||
@.claude/docs/stubs.md
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
|
||||
public enum MutationBehaviour
|
||||
{
|
||||
Wait,
|
||||
DoNotWait,
|
||||
Skip
|
||||
}
|
||||
|
||||
public enum TranslationSource
|
||||
{
|
||||
None,
|
||||
Guess,
|
||||
CSV,
|
||||
PO
|
||||
}
|
||||
|
||||
public partial class DialogueManager : RefCounted
|
||||
{
|
||||
public delegate void DialogueStartedEventHandler(Resource dialogueResource);
|
||||
public delegate void PassedTitleEventHandler(string title);
|
||||
public delegate void GotDialogueEventHandler(DialogueLine dialogueLine);
|
||||
public delegate void MutatedEventHandler(Dictionary mutation);
|
||||
public delegate void DialogueEndedEventHandler(Resource dialogueResource);
|
||||
|
||||
public static DialogueStartedEventHandler? DialogueStarted;
|
||||
public static PassedTitleEventHandler? PassedTitle;
|
||||
public static GotDialogueEventHandler? GotDialogue;
|
||||
public static MutatedEventHandler? Mutated;
|
||||
public static DialogueEndedEventHandler? DialogueEnded;
|
||||
|
||||
[Signal] public delegate void ResolvedEventHandler(Variant value);
|
||||
|
||||
private static Random random = new Random();
|
||||
|
||||
private static GodotObject? instance;
|
||||
public static GodotObject Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = Engine.GetSingleton("DialogueManager");
|
||||
instance.Connect("dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource)));
|
||||
instance.Connect("passed_title", Callable.From((string title) => PassedTitle?.Invoke(title)));
|
||||
instance.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line))));
|
||||
instance.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation)));
|
||||
instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource)));
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Godot.Collections.Array GameStates
|
||||
{
|
||||
get => (Godot.Collections.Array)Instance.Get("game_states");
|
||||
set => Instance.Set("game_states", value);
|
||||
}
|
||||
|
||||
|
||||
public static bool IncludeSingletons
|
||||
{
|
||||
get => (bool)Instance.Get("include_singletons");
|
||||
set => Instance.Set("include_singletons", value);
|
||||
}
|
||||
|
||||
|
||||
public static bool IncludeClasses
|
||||
{
|
||||
get => (bool)Instance.Get("include_classes");
|
||||
set => Instance.Set("include_classes", value);
|
||||
}
|
||||
|
||||
|
||||
public static TranslationSource TranslationSource
|
||||
{
|
||||
get => (TranslationSource)(int)Instance.Get("translation_source");
|
||||
set => Instance.Set("translation_source", (int)value);
|
||||
}
|
||||
|
||||
|
||||
public static Func<Node> GetCurrentScene
|
||||
{
|
||||
set => Instance.Set("get_current_scene", Callable.From(value));
|
||||
}
|
||||
|
||||
public static Resource CreateResourceFromText(string text)
|
||||
{
|
||||
return (Resource)Instance.Call("create_resource_from_text", text);
|
||||
}
|
||||
|
||||
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null, MutationBehaviour mutation_behaviour = MutationBehaviour.Wait)
|
||||
{
|
||||
int id = random.Next();
|
||||
Instance.Call("_bridge_get_next_dialogue_line", id, dialogueResource, key, extraGameStates ?? new Array<Variant>(), (int)mutation_behaviour);
|
||||
while (true)
|
||||
{
|
||||
var result = await Instance.ToSignal(Instance, "bridge_get_next_dialogue_line_completed");
|
||||
if ((int)result[0] == id)
|
||||
{
|
||||
return ((RefCounted)result[1] == null) ? null : new DialogueLine((RefCounted)result[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<DialogueLine?> GetLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
int id = random.Next();
|
||||
Instance.Call("_bridge_get_line", id, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
while (true)
|
||||
{
|
||||
var result = await Instance.ToSignal(Instance, "bridge_get_line_completed");
|
||||
if ((int)result[0] == id)
|
||||
{
|
||||
return ((RefCounted)result[0] == null) ? null : new DialogueLine((RefCounted)result[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
|
||||
public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
|
||||
public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
|
||||
public static Array<string> StaticIdToLineIds(Resource dialogueResource, string staticId)
|
||||
{
|
||||
return (Array<string>)Instance.Call("static_id_to_line_ids", dialogueResource, staticId);
|
||||
}
|
||||
|
||||
|
||||
public static string StaticIdToLineId(Resource dialogueResource, string staticId)
|
||||
{
|
||||
return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId);
|
||||
}
|
||||
|
||||
|
||||
public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false)
|
||||
{
|
||||
int id = random.Next();
|
||||
Instance.Call("_bridge_mutate", id, mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation);
|
||||
while (true)
|
||||
{
|
||||
var result = await Instance.ToSignal(Instance, "bridge_mutated");
|
||||
if ((int)result[0] == id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Array<Dictionary> GetMembersForScript(Script script)
|
||||
{
|
||||
Array<Dictionary> members = new Array<Dictionary>();
|
||||
|
||||
string typeName = script.ResourcePath.GetFile().GetBaseName();
|
||||
var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName);
|
||||
foreach (var matchingType in matchingTypes)
|
||||
{
|
||||
var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var memberInfo in memberInfos)
|
||||
{
|
||||
string type;
|
||||
switch (memberInfo.MemberType)
|
||||
{
|
||||
case MemberTypes.Field:
|
||||
FieldInfo fieldInfo = memberInfo as FieldInfo;
|
||||
|
||||
if (fieldInfo.FieldType.ToString().Contains("EventHandler"))
|
||||
{
|
||||
type = "signal";
|
||||
}
|
||||
else if (fieldInfo.IsLiteral)
|
||||
{
|
||||
type = "constant";
|
||||
}
|
||||
else
|
||||
{
|
||||
type = "property";
|
||||
}
|
||||
break;
|
||||
case MemberTypes.Method:
|
||||
type = "method";
|
||||
break;
|
||||
|
||||
case MemberTypes.NestedType:
|
||||
type = "constant";
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
members.Add(new Dictionary() {
|
||||
{ "name", memberInfo.Name },
|
||||
{ "type", type }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
|
||||
public bool ThingHasConstant(GodotObject thing, string property)
|
||||
{
|
||||
var memberInfos = thing.GetType().GetMember(property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
return memberInfos.Length > 0;
|
||||
}
|
||||
|
||||
|
||||
public Variant ResolveThingConstant(GodotObject thing, string property)
|
||||
{
|
||||
var memberInfos = thing.GetType().GetMember(property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var memberInfo in memberInfos)
|
||||
{
|
||||
if (memberInfo != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (memberInfo.MemberType)
|
||||
{
|
||||
case MemberTypes.Field:
|
||||
return ConvertValueToVariant((memberInfo as FieldInfo).GetValue(thing));
|
||||
|
||||
case MemberTypes.Property:
|
||||
return ConvertValueToVariant((memberInfo as PropertyInfo).GetValue(thing));
|
||||
|
||||
case MemberTypes.NestedType:
|
||||
var type = thing.GetType().GetNestedType(property);
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return GetEnumAsDictionary(type);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"{property} is not supported by Variant.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"{property} is not a public constant on {thing}");
|
||||
}
|
||||
|
||||
|
||||
Dictionary GetEnumAsDictionary(Type enumType)
|
||||
{
|
||||
Dictionary dictionary = new Dictionary();
|
||||
foreach (var value in enumType.GetEnumValuesAsUnderlyingType())
|
||||
{
|
||||
var key = enumType.GetEnumName(value);
|
||||
if (key != null)
|
||||
{
|
||||
dictionary.Add(key, ConvertValueToVariant(value));
|
||||
}
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
|
||||
Variant ConvertValueToVariant(object value)
|
||||
{
|
||||
if (value == null) return default;
|
||||
|
||||
Type rawType = value.GetType();
|
||||
if (rawType.IsEnum)
|
||||
{
|
||||
var values = GetEnumAsDictionary(rawType);
|
||||
value = values[value.ToString()];
|
||||
}
|
||||
|
||||
return value switch
|
||||
{
|
||||
Variant v => v,
|
||||
bool v => Variant.From(v),
|
||||
byte v => Variant.From((long)v),
|
||||
sbyte v => Variant.From((long)v),
|
||||
short v => Variant.From((long)v),
|
||||
ushort v => Variant.From((long)v),
|
||||
int v => Variant.From((long)v),
|
||||
uint v => Variant.From((long)v),
|
||||
long v => Variant.From(v),
|
||||
ulong v => Variant.From((long)v),
|
||||
float v => Variant.From((double)v),
|
||||
double v => Variant.From(v),
|
||||
string v => Variant.From(v),
|
||||
GodotObject godotObj => Variant.From(godotObj),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public bool ThingHasMethod(GodotObject thing, string method, Array<Variant> args)
|
||||
{
|
||||
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var methodInfo in methodInfos)
|
||||
{
|
||||
if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public async void ResolveThingMethod(float id, GodotObject thing, string method, Array<Variant> args)
|
||||
{
|
||||
MethodInfo? info = null;
|
||||
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var methodInfo in methodInfos)
|
||||
{
|
||||
if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Count(p => !p.HasDefaultValue))
|
||||
{
|
||||
info = methodInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
EmitSignal(SignalName.Resolved, id);
|
||||
return;
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
// Convert the method args to something reflection can handle
|
||||
ParameterInfo[] argTypes = info.GetParameters();
|
||||
object[] _args = new object[argTypes.Length];
|
||||
for (int i = 0; i < argTypes.Length; i++)
|
||||
{
|
||||
// check if args is assignable from derived type
|
||||
if (i < args.Count && args[i].Obj != null)
|
||||
{
|
||||
if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType()))
|
||||
{
|
||||
_args[i] = args[i].Obj;
|
||||
}
|
||||
// fallback to assigning primitive types
|
||||
else
|
||||
{
|
||||
_args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType);
|
||||
}
|
||||
}
|
||||
else if (argTypes[i].DefaultValue != null)
|
||||
{
|
||||
_args[i] = argTypes[i].DefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a single frame wait in case the method returns before signals can listen
|
||||
await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame);
|
||||
|
||||
// invoke method and handle the result based on return type
|
||||
object result = info.Invoke(thing, _args);
|
||||
|
||||
if (result is Task taskResult)
|
||||
{
|
||||
await taskResult;
|
||||
try
|
||||
{
|
||||
object value = taskResult.GetType().GetProperty("Result").GetValue(taskResult);
|
||||
EmitSignal(SignalName.Resolved, id, ConvertValueToVariant(value));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
EmitSignal(SignalName.Resolved, id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitSignal(SignalName.Resolved, id, ConvertValueToVariant(result));
|
||||
}
|
||||
}
|
||||
#nullable enable
|
||||
|
||||
|
||||
public static string GetErrorMessage(int error)
|
||||
{
|
||||
return (string)Instance.Call("_bridge_get_error_message", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public partial class DialogueLine : RefCounted
|
||||
{
|
||||
private string id = "";
|
||||
public string Id
|
||||
{
|
||||
get => id;
|
||||
set => id = value;
|
||||
}
|
||||
|
||||
private string type = "dialogue";
|
||||
public string Type
|
||||
{
|
||||
get => type;
|
||||
set => type = value;
|
||||
}
|
||||
|
||||
private string next_id = "";
|
||||
public string NextId
|
||||
{
|
||||
get => next_id;
|
||||
set => next_id = value;
|
||||
}
|
||||
|
||||
private string character = "";
|
||||
public string Character
|
||||
{
|
||||
get => character;
|
||||
set => character = value;
|
||||
}
|
||||
|
||||
private string text = "";
|
||||
public string Text
|
||||
{
|
||||
get => text;
|
||||
set => text = value;
|
||||
}
|
||||
|
||||
private string translation_key = "";
|
||||
public string TranslationKey
|
||||
{
|
||||
get => translation_key;
|
||||
set => translation_key = value;
|
||||
}
|
||||
|
||||
private Array<DialogueResponse> responses = new Array<DialogueResponse>();
|
||||
public Array<DialogueResponse> Responses
|
||||
{
|
||||
get => responses;
|
||||
}
|
||||
|
||||
private string? time = null;
|
||||
public string? Time
|
||||
{
|
||||
get => time;
|
||||
}
|
||||
|
||||
private Dictionary speeds = new Dictionary();
|
||||
public Dictionary Speeds
|
||||
{
|
||||
get => speeds;
|
||||
}
|
||||
|
||||
private Array<Godot.Collections.Array> inline_mutations = new Array<Godot.Collections.Array>();
|
||||
public Array<Godot.Collections.Array> InlineMutations
|
||||
{
|
||||
get => inline_mutations;
|
||||
}
|
||||
|
||||
private Array<DialogueLine> concurrent_lines = new Array<DialogueLine>();
|
||||
public Array<DialogueLine> ConcurrentLines
|
||||
{
|
||||
get => concurrent_lines;
|
||||
}
|
||||
|
||||
private Array<Variant> extra_game_states = new Array<Variant>();
|
||||
public Array<Variant> ExtraGameStates
|
||||
{
|
||||
get => extra_game_states;
|
||||
}
|
||||
|
||||
private Array<string> tags = new Array<string>();
|
||||
public Array<string> Tags
|
||||
{
|
||||
get => tags;
|
||||
}
|
||||
|
||||
public DialogueLine(RefCounted data)
|
||||
{
|
||||
id = (string)data.Get("id");
|
||||
type = (string)data.Get("type");
|
||||
next_id = (string)data.Get("next_id");
|
||||
character = (string)data.Get("character");
|
||||
text = (string)data.Get("text");
|
||||
translation_key = (string)data.Get("translation_key");
|
||||
speeds = (Dictionary)data.Get("speeds");
|
||||
inline_mutations = (Array<Godot.Collections.Array>)data.Get("inline_mutations");
|
||||
time = (string)data.Get("time");
|
||||
tags = (Array<string>)data.Get("tags");
|
||||
|
||||
foreach (var concurrent_line_data in (Array<RefCounted>)data.Get("concurrent_lines"))
|
||||
{
|
||||
concurrent_lines.Add(new DialogueLine(concurrent_line_data));
|
||||
}
|
||||
|
||||
foreach (var response in (Array<RefCounted>)data.Get("responses"))
|
||||
{
|
||||
responses.Add(new DialogueResponse(response));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool HasTag(string tagName)
|
||||
{
|
||||
string wrapped = $"{tagName}=";
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.StartsWith(wrapped))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public string GetTagValue(string tagName)
|
||||
{
|
||||
string wrapped = $"{tagName}=";
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.StartsWith(wrapped))
|
||||
{
|
||||
return tag.Substring(wrapped.Length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "dialogue":
|
||||
return $"<DialogueLine character=\"{character}\" text=\"{text}\">";
|
||||
case "mutation":
|
||||
return "<DialogueLine mutation>";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public partial class DialogueResponse : RefCounted
|
||||
{
|
||||
private string next_id = "";
|
||||
public string NextId
|
||||
{
|
||||
get => next_id;
|
||||
set => next_id = value;
|
||||
}
|
||||
|
||||
private bool is_allowed = true;
|
||||
public bool IsAllowed
|
||||
{
|
||||
get => is_allowed;
|
||||
set => is_allowed = value;
|
||||
}
|
||||
|
||||
private string condition_as_text = "";
|
||||
public string ConditionAsText
|
||||
{
|
||||
get => condition_as_text;
|
||||
set => condition_as_text = value;
|
||||
}
|
||||
|
||||
private string text = "";
|
||||
public string Text
|
||||
{
|
||||
get => text;
|
||||
set => text = value;
|
||||
}
|
||||
|
||||
private string translation_key = "";
|
||||
public string TranslationKey
|
||||
{
|
||||
get => translation_key;
|
||||
set => translation_key = value;
|
||||
}
|
||||
|
||||
private Array<string> tags = new Array<string>();
|
||||
public Array<string> Tags
|
||||
{
|
||||
get => tags;
|
||||
}
|
||||
|
||||
public DialogueResponse(RefCounted data)
|
||||
{
|
||||
next_id = (string)data.Get("next_id");
|
||||
is_allowed = (bool)data.Get("is_allowed");
|
||||
text = (string)data.Get("text");
|
||||
translation_key = (string)data.Get("translation_key");
|
||||
tags = (Array<string>)data.Get("tags");
|
||||
}
|
||||
|
||||
public string GetTagValue(string tagName)
|
||||
{
|
||||
string wrapped = $"{tagName}=";
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.StartsWith(wrapped))
|
||||
{
|
||||
return tag.Substring(wrapped.Length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"<DialogueResponse text=\"{text}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4c5lsrwy3opj
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-present Nathan Hoad and Dialogue Manager contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cnm67htuohhlo"
|
||||
path="res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/banner.png"
|
||||
dest_files=["res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3lr2uas6ax8v"
|
||||
path="res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 4.2333333 4.2333335"
|
||||
version="1.1"
|
||||
id="svg291"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="responses_menu.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview293"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
width="1920px"
|
||||
units="px"
|
||||
borderlayer="true"
|
||||
inkscape:showpageshadow="false"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="45.254834"
|
||||
inkscape:cx="7.8334173"
|
||||
inkscape:cy="6.5959804"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1377"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs288" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect181"
|
||||
style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.77487;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke markers fill"
|
||||
d="M 1.5875 0.26458334 L 1.5875 0.79375001 L 4.2333334 0.79375001 L 4.2333334 0.26458334 L 1.5875 0.26458334 z M 0 0.83147381 L 0 2.4189738 L 1.3229167 1.6252238 L 0 0.83147381 z M 1.5875 1.3229167 L 1.5875 1.8520834 L 4.2333334 1.8520834 L 4.2333334 1.3229167 L 1.5875 1.3229167 z M 1.5875 2.38125 L 1.5875 2.9104167 L 4.2333334 2.9104167 L 4.2333334 2.38125 L 1.5875 2.38125 z M 1.5875 3.4395834 L 1.5875 3.9687501 L 4.2333334 3.9687501 L 4.2333334 3.4395834 L 1.5875 3.4395834 z "
|
||||
fill="#E0E0E0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://drjfciwitjm83"
|
||||
path="res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/responses_menu.svg"
|
||||
dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3baj6rygkb3f"
|
||||
path="res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/update.svg"
|
||||
dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://dsgpnyqg6cprg
|
||||
@@ -0,0 +1,161 @@
|
||||
## A compiled line of dialogue.
|
||||
class_name DMCompiledLine extends RefCounted
|
||||
|
||||
|
||||
## The ID of the line
|
||||
var id: String
|
||||
## The translation key (or static line ID).
|
||||
var translation_key: String = ""
|
||||
## The type of line.
|
||||
var type: String = ""
|
||||
## The character name.
|
||||
var character: String = ""
|
||||
## Any interpolation expressions for the character name.
|
||||
var character_replacements: Array[Dictionary] = []
|
||||
## The text of the line.
|
||||
var text: String = ""
|
||||
## Any interpolation expressions for the text.
|
||||
var text_replacements: Array[Dictionary] = []
|
||||
## Any response siblings associated with this line.
|
||||
var responses: PackedStringArray = []
|
||||
## Any randomise or case siblings for this line.
|
||||
var siblings: Array[Dictionary] = []
|
||||
## Any lines said simultaneously.
|
||||
var concurrent_lines: PackedStringArray = []
|
||||
## Any tags on this line.
|
||||
var tags: PackedStringArray = []
|
||||
## The condition or mutation expression for this line.
|
||||
var expression: Dictionary = {}
|
||||
## The express as the raw text that was given.
|
||||
var expression_text: String = ""
|
||||
## The next sequential line to go to after this line.
|
||||
var next_id: String = ""
|
||||
## The next line to go to after this line if it is unknown and compile time.
|
||||
var next_id_expression: Array[Dictionary] = []
|
||||
## Whether this jump line should return after the jump target sequence has ended.
|
||||
var is_snippet: bool = false
|
||||
## The ID of the next sibling line.
|
||||
var next_sibling_id: String = ""
|
||||
## The ID after this line if it belongs to a block (eg. conditions).
|
||||
var next_id_after: String = ""
|
||||
## Any doc comments attached to this line.
|
||||
var notes: String = ""
|
||||
|
||||
|
||||
#region Hooks
|
||||
|
||||
|
||||
func _init(initial_id: String, initial_type: String) -> void:
|
||||
id = initial_id
|
||||
type = initial_type
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
var s: Array = [
|
||||
"[%s]" % [type],
|
||||
"%s:" % [character] if character != "" else null,
|
||||
text if text != "" else null,
|
||||
expression if expression.size() > 0 else null,
|
||||
"[%s]" % [",".join(tags)] if tags.size() > 0 else null,
|
||||
str(siblings) if siblings.size() > 0 else null,
|
||||
str(responses) if responses.size() > 0 else null,
|
||||
"=> END" if "end" in next_id else "=> %s" % [next_id],
|
||||
"(~> %s)" % [next_sibling_id] if next_sibling_id != "" else null,
|
||||
"(==> %s)" % [next_id_after] if next_id_after != "" else null,
|
||||
].filter(func(item): return item != null)
|
||||
|
||||
return " ".join(s)
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
## Express this line as a [Dictionary] that can be stored in a resource.
|
||||
func to_data() -> Dictionary:
|
||||
var d: Dictionary = {
|
||||
id = id,
|
||||
type = type,
|
||||
next_id = next_id
|
||||
}
|
||||
|
||||
if next_id_expression.size() > 0:
|
||||
d.next_id_expression = next_id_expression
|
||||
|
||||
match type:
|
||||
DMConstants.TYPE_CONDITION:
|
||||
d.condition = expression
|
||||
if not next_sibling_id.is_empty():
|
||||
d.next_sibling_id = next_sibling_id
|
||||
d.next_id_after = next_id_after
|
||||
|
||||
DMConstants.TYPE_WHILE:
|
||||
d.condition = expression
|
||||
d.next_id_after = next_id_after
|
||||
|
||||
DMConstants.TYPE_MATCH:
|
||||
d.condition = expression
|
||||
d.next_id_after = next_id_after
|
||||
d.cases = siblings
|
||||
|
||||
DMConstants.TYPE_MUTATION:
|
||||
d.mutation = expression
|
||||
|
||||
DMConstants.TYPE_GOTO:
|
||||
d.is_snippet = is_snippet
|
||||
d.next_id_after = next_id_after
|
||||
if not siblings.is_empty():
|
||||
d.siblings = siblings
|
||||
|
||||
DMConstants.TYPE_RANDOM:
|
||||
d.siblings = siblings
|
||||
|
||||
DMConstants.TYPE_RESPONSE:
|
||||
d.text = text
|
||||
|
||||
if not responses.is_empty():
|
||||
d.responses = responses
|
||||
|
||||
if translation_key != text:
|
||||
d.translation_key = translation_key
|
||||
if not expression.is_empty():
|
||||
d.condition = expression
|
||||
if not character.is_empty():
|
||||
d.character = character
|
||||
if not character_replacements.is_empty():
|
||||
d.character_replacements = character_replacements
|
||||
if not text_replacements.is_empty():
|
||||
d.text_replacements = text_replacements
|
||||
if not tags.is_empty():
|
||||
d.tags = tags
|
||||
if not notes.is_empty():
|
||||
d.notes = notes
|
||||
if not expression_text.is_empty():
|
||||
d.condition_as_text = expression_text
|
||||
|
||||
DMConstants.TYPE_DIALOGUE:
|
||||
d.text = text
|
||||
|
||||
if translation_key != text:
|
||||
d.translation_key = translation_key
|
||||
|
||||
if not character.is_empty():
|
||||
d.character = character
|
||||
if not character_replacements.is_empty():
|
||||
d.character_replacements = character_replacements
|
||||
if not text_replacements.is_empty():
|
||||
d.text_replacements = text_replacements
|
||||
if not tags.is_empty():
|
||||
d.tags = tags
|
||||
if not notes.is_empty():
|
||||
d.notes = notes
|
||||
if not siblings.is_empty():
|
||||
d.siblings = siblings
|
||||
if not concurrent_lines.is_empty():
|
||||
d.concurrent_lines = concurrent_lines
|
||||
|
||||
return d
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://dg8j5hudp4210
|
||||
@@ -0,0 +1,58 @@
|
||||
## A compiler of Dialogue Manager dialogue.
|
||||
class_name DMCompiler extends RefCounted
|
||||
|
||||
|
||||
## Compile a dialogue script.
|
||||
static func compile_string(text: String, path: String) -> DMCompilerResult:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
compilation.compile(text, path)
|
||||
|
||||
var result: DMCompilerResult = DMCompilerResult.new()
|
||||
result.imported_paths = compilation.imported_paths
|
||||
result.using_states = compilation.using_states
|
||||
result.character_names = compilation.character_names
|
||||
result.titles = compilation.titles
|
||||
result.first_title = compilation.first_title
|
||||
result.errors = compilation.errors
|
||||
result.lines = compilation.data
|
||||
result.raw_text = text
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Get the line type of a string. The returned string will match one of the [code]TYPE_[/code] constants of [DMConstants].
|
||||
static func get_line_type(text: String) -> String:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
return compilation.get_line_type(text)
|
||||
|
||||
|
||||
## Get the static line ID (eg. [code][ID:SOMETHING][/code]) of some text.
|
||||
static func get_static_line_id(text: String) -> String:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
return compilation.extract_static_line_id(text)
|
||||
|
||||
|
||||
## Get the translatable part of a line.
|
||||
static func extract_translatable_string(text: String) -> String:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
|
||||
var tree_line = DMTreeLine.new("")
|
||||
tree_line.text = text
|
||||
var line: DMCompiledLine = DMCompiledLine.new("", compilation.get_line_type(text))
|
||||
compilation.parse_character_and_dialogue(tree_line, line, [tree_line], 0, null)
|
||||
|
||||
return line.text
|
||||
|
||||
|
||||
## Extract a mutation from a string.
|
||||
static func extract_mutation(text: String) -> Dictionary:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
return compilation.extract_mutation(text)
|
||||
|
||||
|
||||
## Get the known titles in a dialogue script.
|
||||
static func get_titles_in_text(text: String, path: String) -> Dictionary:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
compilation.find_imported_titles(text, path)
|
||||
compilation.build_line_tree(text.split("\n"))
|
||||
return compilation.titles
|
||||
@@ -0,0 +1 @@
|
||||
uid://chtfdmr0cqtp4
|
||||
@@ -0,0 +1,50 @@
|
||||
## A collection of [RegEx] for use by the [DMCompiler].
|
||||
class_name DMCompilerRegEx extends RefCounted
|
||||
|
||||
|
||||
var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?<path>[^\"]+)\" as (?<prefix>[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+)")
|
||||
var USING_REGEX: RegEx = RegEx.create_from_string("^using (?<state>.*)$")
|
||||
var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+")
|
||||
var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*$")
|
||||
var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d")
|
||||
var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if|match|when) (?<expression>.*)\\:?")
|
||||
var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?<expression>.*)\\]")
|
||||
var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?<keyword>\\$\\>|\\$\\>\\>|do|do!|set) (?<expression>.*)")
|
||||
var STATIC_LINE_ID_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?<id>.*?)\\]")
|
||||
var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>[\\d.]+)?( \\[if (?<condition>.+?)\\])? ")
|
||||
var GOTO_REGEX: RegEx = RegEx.create_from_string("=><? (?<goto>.*)")
|
||||
|
||||
var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
|
||||
var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.+?)\\](?<body>.*?)\\[\\/if\\]")
|
||||
|
||||
var IMAGE_TAGS_REGEX: RegEx = RegEx.create_from_string("\\[img.*?\\](?<path>.+?)\\[\\/img\\]")
|
||||
|
||||
var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?<tags>.*?)\\]")
|
||||
|
||||
var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}")
|
||||
|
||||
var TOKEN_DEFINITIONS: Dictionary = {
|
||||
DMConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\("),
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\["),
|
||||
DMConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("),
|
||||
DMConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"),
|
||||
DMConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["),
|
||||
DMConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"),
|
||||
DMConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"),
|
||||
DMConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"),
|
||||
DMConstants.TOKEN_COLON: RegEx.create_from_string("^:"),
|
||||
DMConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"),
|
||||
DMConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"),
|
||||
DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"),
|
||||
DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"),
|
||||
DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"),
|
||||
DMConstants.TOKEN_NULL_COALESCE: RegEx.create_from_string("^\\?\\."),
|
||||
DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."),
|
||||
DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"),
|
||||
DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"),
|
||||
DMConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"),
|
||||
DMConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*"),
|
||||
DMConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"),
|
||||
DMConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"),
|
||||
DMConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3tvcrnicjibp
|
||||
@@ -0,0 +1,27 @@
|
||||
## The result of using the [DMCompiler] to compile some dialogue.
|
||||
class_name DMCompilerResult extends RefCounted
|
||||
|
||||
|
||||
## Any paths that were imported into the compiled dialogue file.
|
||||
var imported_paths: PackedStringArray = []
|
||||
|
||||
## Any "using" directives.
|
||||
var using_states: PackedStringArray = []
|
||||
|
||||
## All titles in the file and the line they point to.
|
||||
var titles: Dictionary = {}
|
||||
|
||||
## The first title in the file.
|
||||
var first_title: String = ""
|
||||
|
||||
## All character names.
|
||||
var character_names: PackedStringArray = []
|
||||
|
||||
## Any compilation errors.
|
||||
var errors: Array[Dictionary] = []
|
||||
|
||||
## A map of all compiled lines.
|
||||
var lines: Dictionary = {}
|
||||
|
||||
## The raw dialogue text.
|
||||
var raw_text: String = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmk74tknimqvg
|
||||
@@ -0,0 +1,529 @@
|
||||
## A class for parsing a condition/mutation expression for use with the [DMCompiler].
|
||||
class_name DMExpressionParser extends RefCounted
|
||||
|
||||
|
||||
var include_comments: bool = false
|
||||
|
||||
|
||||
# Reference to the common [RegEx] that the parser needs.
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
|
||||
|
||||
## Break a string down into an expression.
|
||||
func tokenise(text: String, line_type: String, index: int) -> Array:
|
||||
var tokens: Array[Dictionary] = []
|
||||
var limit: int = 0
|
||||
while text.strip_edges() != "" and limit < 1000:
|
||||
limit += 1
|
||||
var found = _find_match(text)
|
||||
if found.size() > 0:
|
||||
tokens.append({
|
||||
index = index,
|
||||
type = found.type,
|
||||
value = found.value
|
||||
})
|
||||
index += found.value.length()
|
||||
text = found.remaining_text
|
||||
elif text.begins_with(" "):
|
||||
index += 1
|
||||
text = text.substr(1)
|
||||
else:
|
||||
return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index)
|
||||
|
||||
return _build_token_tree(tokens, line_type, "")[0]
|
||||
|
||||
|
||||
## Extract any expressions from some text
|
||||
func extract_replacements(text: String, index: int) -> Array[Dictionary]:
|
||||
var founds: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(text)
|
||||
|
||||
if founds == null or founds.size() == 0:
|
||||
return []
|
||||
|
||||
var replacements: Array[Dictionary] = []
|
||||
for found in founds:
|
||||
var replacement: Dictionary = {}
|
||||
var value_in_text: String = found.strings[0].substr(0, found.strings[0].length() - 2).substr(2)
|
||||
|
||||
# If there are closing curlie hard-up against the end of a {{...}} block then check for further
|
||||
# curlies just outside of the block.
|
||||
var text_suffix: String = text.substr(found.get_end(0))
|
||||
var expression_suffix: String = ""
|
||||
while text_suffix.begins_with("}"):
|
||||
expression_suffix += "}"
|
||||
text_suffix = text_suffix.substr(1)
|
||||
value_in_text += expression_suffix
|
||||
|
||||
var expression: Array = tokenise(value_in_text, DMConstants.TYPE_DIALOGUE, index + found.get_start(1))
|
||||
if expression.size() == 0:
|
||||
replacement = {
|
||||
index = index + found.get_start(1),
|
||||
error = DMConstants.ERR_INCOMPLETE_EXPRESSION
|
||||
}
|
||||
elif expression[0].type == DMConstants.TYPE_ERROR:
|
||||
replacement = {
|
||||
index = expression[0].i,
|
||||
error = expression[0].value
|
||||
}
|
||||
else:
|
||||
replacement = {
|
||||
value_in_text = "{{%s}}" % value_in_text,
|
||||
expression = expression
|
||||
}
|
||||
replacements.append(replacement)
|
||||
|
||||
return replacements
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
# Create a token that represents an error.
|
||||
func _build_token_tree_error(tree: Array, error: int, index: int) -> Array:
|
||||
tree.insert(0, {
|
||||
type = DMConstants.TOKEN_ERROR,
|
||||
value = error,
|
||||
i = index
|
||||
})
|
||||
return tree
|
||||
|
||||
|
||||
# Convert a list of tokens into an abstract syntax tree.
|
||||
func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array:
|
||||
var tree: Array[Dictionary] = []
|
||||
var limit = 0
|
||||
while tokens.size() > 0 and limit < 1000:
|
||||
limit += 1
|
||||
var token = tokens.pop_front()
|
||||
|
||||
var error = _check_next_token(token, tokens, line_type, expected_close_token)
|
||||
if error != OK:
|
||||
var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token
|
||||
return [_build_token_tree_error(tree, error, error_token.index), tokens]
|
||||
|
||||
match token.type:
|
||||
DMConstants.TOKEN_COMMENT:
|
||||
if include_comments:
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_COMMENT,
|
||||
value = token.value,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_FUNCTION:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_FUNCTION,
|
||||
# Consume the trailing "("
|
||||
function = token.value.substr(0, token.value.length() - 1),
|
||||
value = _tokens_to_list(sub_tree[0]),
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
var args = _tokens_to_list(sub_tree[0])
|
||||
if args.size() != 1:
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens]
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_DICTIONARY_REFERENCE,
|
||||
# Consume the trailing "["
|
||||
variable = token.value.substr(0, token.value.length() - 1),
|
||||
value = args[0],
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_BRACE_OPEN:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
var t = sub_tree[0]
|
||||
for i in range(0, t.size() - 2):
|
||||
# Convert Lua style dictionaries to string keys
|
||||
if t[i].type == DMConstants.TOKEN_VARIABLE and t[i+1].type == DMConstants.TOKEN_ASSIGNMENT:
|
||||
t[i].type = DMConstants.TOKEN_STRING
|
||||
t[i+1].type = DMConstants.TOKEN_COLON
|
||||
t[i+1].erase("value")
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_DICTIONARY,
|
||||
value = _tokens_to_dictionary(sub_tree[0]),
|
||||
i = token.index
|
||||
})
|
||||
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_BRACKET_OPEN:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
var type = DMConstants.TOKEN_ARRAY
|
||||
var value = _tokens_to_list(sub_tree[0])
|
||||
|
||||
# See if this is referencing a nested dictionary value
|
||||
if tree.size() > 0:
|
||||
var previous_token = tree[tree.size() - 1]
|
||||
if previous_token.type in [DMConstants.TOKEN_DICTIONARY_REFERENCE, DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]:
|
||||
type = DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE
|
||||
value = value[0]
|
||||
|
||||
tree.append({
|
||||
type = type,
|
||||
value = value,
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_PARENS_OPEN:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_GROUP,
|
||||
value = sub_tree[0],
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_PARENS_CLOSE, \
|
||||
DMConstants.TOKEN_BRACE_CLOSE, \
|
||||
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||
if token.type != expected_close_token:
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens]
|
||||
|
||||
tree.append({
|
||||
type = token.type,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
return [tree, tokens]
|
||||
|
||||
DMConstants.TOKEN_NOT:
|
||||
# Double nots negate each other
|
||||
if tokens.size() > 0 and tokens.front().type == DMConstants.TOKEN_NOT:
|
||||
tokens.pop_front()
|
||||
else:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_COMMA, \
|
||||
DMConstants.TOKEN_COLON, \
|
||||
DMConstants.TOKEN_DOT, \
|
||||
DMConstants.TOKEN_NULL_COALESCE:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_COMPARISON, \
|
||||
DMConstants.TOKEN_ASSIGNMENT, \
|
||||
DMConstants.TOKEN_OPERATOR, \
|
||||
DMConstants.TOKEN_AND_OR, \
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
var value = token.value.strip_edges()
|
||||
if value == "&&":
|
||||
value = "and"
|
||||
elif value == "||":
|
||||
value = "or"
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = value,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_STRING:
|
||||
if token.value.begins_with("&"):
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = StringName(token.value.substr(2, token.value.length() - 3)),
|
||||
i = token.index
|
||||
})
|
||||
else:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = token.value.substr(1, token.value.length() - 2),
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_CONDITION:
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token]
|
||||
|
||||
DMConstants.TOKEN_BOOL:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = token.value.to_lower() == "true",
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_NUMBER:
|
||||
var value = token.value.to_float() if "." in token.value else token.value.to_int()
|
||||
# If previous token is a number and this one is a negative number then
|
||||
# inject a minus operator token in between them.
|
||||
if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DMConstants.TOKEN_NUMBER:
|
||||
tree.append(({
|
||||
type = DMConstants.TOKEN_OPERATOR,
|
||||
value = "-",
|
||||
i = token.index
|
||||
}))
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = -1 * value,
|
||||
i = token.index
|
||||
})
|
||||
else:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = value,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
if expected_close_token != "":
|
||||
var index: int = tokens[0].i if tokens.size() > 0 else 0
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens]
|
||||
|
||||
return [tree, tokens]
|
||||
|
||||
|
||||
# Check the next token to see if it is valid to follow this one.
|
||||
func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error:
|
||||
var next_token: Dictionary = { type = null }
|
||||
if next_tokens.size() > 0:
|
||||
next_token = next_tokens.front()
|
||||
|
||||
# Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary
|
||||
# then it's an unexpected assignment in a condition line.
|
||||
if token.type == DMConstants.TOKEN_ASSIGNMENT and line_type == DMConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token):
|
||||
return DMConstants.ERR_UNEXPECTED_ASSIGNMENT
|
||||
|
||||
# Special case for a negative number after this one
|
||||
if token.type == DMConstants.TOKEN_NUMBER and next_token.type == DMConstants.TOKEN_NUMBER and next_token.value.begins_with("-"):
|
||||
return OK
|
||||
|
||||
var expected_token_types = []
|
||||
var unexpected_token_types = []
|
||||
match token.type:
|
||||
DMConstants.TOKEN_FUNCTION, \
|
||||
DMConstants.TOKEN_PARENS_OPEN:
|
||||
unexpected_token_types = [
|
||||
null,
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_COMPARISON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_BRACE_OPEN:
|
||||
expected_token_types = [
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_VARIABLE,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_BRACE_CLOSE
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_PARENS_CLOSE, \
|
||||
DMConstants.TOKEN_BRACE_CLOSE:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_COMPARISON, \
|
||||
DMConstants.TOKEN_OPERATOR, \
|
||||
DMConstants.TOKEN_DOT, \
|
||||
DMConstants.TOKEN_NULL_COALESCE, \
|
||||
DMConstants.TOKEN_NOT, \
|
||||
DMConstants.TOKEN_AND_OR, \
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||
unexpected_token_types = [
|
||||
null,
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_COMPARISON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_PARENS_CLOSE,
|
||||
DMConstants.TOKEN_BRACE_CLOSE,
|
||||
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_COMMA:
|
||||
unexpected_token_types = [
|
||||
null,
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_PARENS_CLOSE,
|
||||
DMConstants.TOKEN_BRACE_CLOSE,
|
||||
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_COLON:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_COMPARISON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_PARENS_CLOSE,
|
||||
DMConstants.TOKEN_BRACE_CLOSE,
|
||||
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_BOOL, \
|
||||
DMConstants.TOKEN_STRING, \
|
||||
DMConstants.TOKEN_NUMBER:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE,
|
||||
DMConstants.TOKEN_FUNCTION,
|
||||
DMConstants.TOKEN_PARENS_OPEN,
|
||||
DMConstants.TOKEN_BRACE_OPEN,
|
||||
DMConstants.TOKEN_BRACKET_OPEN
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE,
|
||||
DMConstants.TOKEN_FUNCTION,
|
||||
DMConstants.TOKEN_PARENS_OPEN,
|
||||
DMConstants.TOKEN_BRACE_OPEN,
|
||||
DMConstants.TOKEN_BRACKET_OPEN
|
||||
]
|
||||
|
||||
if (expected_token_types.size() > 0 and not next_token.type in expected_token_types) \
|
||||
or (unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types):
|
||||
match next_token.type:
|
||||
null:
|
||||
return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION
|
||||
|
||||
DMConstants.TOKEN_FUNCTION:
|
||||
return DMConstants.ERR_UNEXPECTED_FUNCTION
|
||||
|
||||
DMConstants.TOKEN_PARENS_OPEN, \
|
||||
DMConstants.TOKEN_PARENS_CLOSE:
|
||||
return DMConstants.ERR_UNEXPECTED_BRACKET
|
||||
|
||||
DMConstants.TOKEN_COMPARISON, \
|
||||
DMConstants.TOKEN_ASSIGNMENT, \
|
||||
DMConstants.TOKEN_OPERATOR, \
|
||||
DMConstants.TOKEN_NOT, \
|
||||
DMConstants.TOKEN_AND_OR:
|
||||
return DMConstants.ERR_UNEXPECTED_OPERATOR
|
||||
|
||||
DMConstants.TOKEN_COMMA:
|
||||
return DMConstants.ERR_UNEXPECTED_COMMA
|
||||
DMConstants.TOKEN_COLON:
|
||||
return DMConstants.ERR_UNEXPECTED_COLON
|
||||
DMConstants.TOKEN_DOT:
|
||||
return DMConstants.ERR_UNEXPECTED_DOT
|
||||
|
||||
DMConstants.TOKEN_BOOL:
|
||||
return DMConstants.ERR_UNEXPECTED_BOOLEAN
|
||||
DMConstants.TOKEN_STRING:
|
||||
return DMConstants.ERR_UNEXPECTED_STRING
|
||||
DMConstants.TOKEN_NUMBER:
|
||||
return DMConstants.ERR_UNEXPECTED_NUMBER
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
return DMConstants.ERR_UNEXPECTED_VARIABLE
|
||||
|
||||
return DMConstants.ERR_INVALID_EXPRESSION
|
||||
|
||||
return OK
|
||||
|
||||
|
||||
# Convert a series of comma separated tokens to an [Array].
|
||||
func _tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]:
|
||||
var list: Array[Array] = []
|
||||
var current_item: Array[Dictionary] = []
|
||||
for token in tokens:
|
||||
if token.type == DMConstants.TOKEN_COMMA:
|
||||
list.append(current_item)
|
||||
current_item = []
|
||||
else:
|
||||
current_item.append(token)
|
||||
|
||||
if current_item.size() > 0:
|
||||
list.append(current_item)
|
||||
|
||||
return list
|
||||
|
||||
|
||||
# Convert a series of key/value tokens into a [Dictionary]
|
||||
func _tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary:
|
||||
var dictionary = {}
|
||||
for i in range(0, tokens.size()):
|
||||
if tokens[i].type == DMConstants.TOKEN_COLON:
|
||||
if tokens.size() == i + 2:
|
||||
dictionary[tokens[i - 1]] = tokens[i + 1]
|
||||
else:
|
||||
dictionary[tokens[i - 1]] = { type = DMConstants.TOKEN_GROUP, value = tokens.slice(i + 1), i = tokens[0].i }
|
||||
|
||||
return dictionary
|
||||
|
||||
|
||||
# Work out what the next token is from a string.
|
||||
func _find_match(input: String) -> Dictionary:
|
||||
for key in regex.TOKEN_DEFINITIONS.keys():
|
||||
var regex = regex.TOKEN_DEFINITIONS.get(key)
|
||||
var found = regex.search(input)
|
||||
if found:
|
||||
return {
|
||||
type = key,
|
||||
remaining_text = input.substr(found.strings[0].length()),
|
||||
value = found.strings[0]
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbi4hbar8ubwu
|
||||
@@ -0,0 +1,70 @@
|
||||
## Data associated with a dialogue jump/goto line.
|
||||
class_name DMResolvedGotoData extends RefCounted
|
||||
|
||||
|
||||
## The title that was specified
|
||||
var title: String = ""
|
||||
## The target line's ID
|
||||
var next_id: String = ""
|
||||
## An expression to determine the target line at runtime.
|
||||
var expression: Array[Dictionary] = []
|
||||
## The given line text with the jump syntax removed.
|
||||
var text_without_goto: String = ""
|
||||
## Whether this is a jump-and-return style jump.
|
||||
var is_snippet: bool = false
|
||||
## A parse error if there was one.
|
||||
var error: int
|
||||
## The index in the string where
|
||||
var index: int = 0
|
||||
|
||||
# An instance of the compiler [RegEx] list.
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
|
||||
|
||||
func _init(text: String, titles: Dictionary) -> void:
|
||||
if not "=> " in text and not "=>< " in text: return
|
||||
|
||||
if "=> " in text:
|
||||
text_without_goto = text.substr(0, text.find("=> ")).strip_edges()
|
||||
elif "=>< " in text:
|
||||
is_snippet = true
|
||||
text_without_goto = text.substr(0, text.find("=>< ")).strip_edges()
|
||||
|
||||
var found: RegExMatch = regex.GOTO_REGEX.search(text)
|
||||
if found == null:
|
||||
return
|
||||
|
||||
title = found.strings[found.names.goto].strip_edges()
|
||||
index = found.get_start(0)
|
||||
|
||||
if title == "":
|
||||
error = DMConstants.ERR_UNKNOWN_TITLE
|
||||
return
|
||||
|
||||
# "=> END!" means end the conversation, ignoring any "=><" chains.
|
||||
if title == "END!":
|
||||
next_id = DMConstants.ID_END_CONVERSATION
|
||||
|
||||
# "=> END" means end the current title (and go back to the previous one if there is one
|
||||
# in the stack)
|
||||
elif title == "END":
|
||||
next_id = DMConstants.ID_END
|
||||
|
||||
elif titles.has(title):
|
||||
next_id = titles.get(title)
|
||||
elif title.begins_with("{{"):
|
||||
var expression_parser: DMExpressionParser = DMExpressionParser.new()
|
||||
var title_expression: Array[Dictionary] = expression_parser.extract_replacements(title, 0)
|
||||
if title_expression.size() == 0:
|
||||
error = DMConstants.ERR_INCOMPLETE_EXPRESSION
|
||||
elif title_expression[0].has("error"):
|
||||
error = title_expression[0].error
|
||||
else:
|
||||
expression = title_expression[0].expression
|
||||
else:
|
||||
next_id = title
|
||||
error = DMConstants.ERR_UNKNOWN_TITLE
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "%s =>%s %s (%s)" % [text_without_goto, "<" if is_snippet else "", title, next_id]
|
||||
@@ -0,0 +1 @@
|
||||
uid://llhl5pt47eoq
|
||||
@@ -0,0 +1,160 @@
|
||||
## Any data associated with inline dialogue BBCodes.
|
||||
class_name DMResolvedLineData extends RefCounted
|
||||
|
||||
## The line's text
|
||||
var text: String = ""
|
||||
## A map of speed changes against where they are found in the text.
|
||||
var speeds: Dictionary = {}
|
||||
## A list of any mutations to run and where they are found in the text.
|
||||
var mutations: Array[Array] = []
|
||||
## A duration reference for the line. Represented as "auto" or a stringified number.
|
||||
var time: String = ""
|
||||
|
||||
|
||||
func _init(line: String) -> void:
|
||||
text = line
|
||||
speeds = {}
|
||||
mutations = []
|
||||
time = ""
|
||||
|
||||
var bbcodes: Array = []
|
||||
|
||||
# Remove any escaped brackets (ie. "\[")
|
||||
var escaped_open_brackets: PackedInt32Array = []
|
||||
var escaped_close_brackets: PackedInt32Array = []
|
||||
for i in range(0, text.length() - 1):
|
||||
if text.substr(i, 2) == "\\[":
|
||||
text = text.substr(0, i) + "!" + text.substr(i + 2)
|
||||
escaped_open_brackets.append(i)
|
||||
elif text.substr(i, 2) == "\\]":
|
||||
text = text.substr(0, i) + "!" + text.substr(i + 2)
|
||||
escaped_close_brackets.append(i)
|
||||
|
||||
# Extract all of the BB codes so that we know the actual text (we could do this easier with
|
||||
# a RichTextLabel but then we'd need to await idle_frame which is annoying)
|
||||
var bbcode_positions = find_bbcode_positions_in_string(text)
|
||||
var accumulaive_length_offset = 0
|
||||
for position in bbcode_positions:
|
||||
# Ignore our own markers
|
||||
if position.code in ["wait", "speed", "/speed", "$>", "$>>", "do", "do!", "set", "next", "if", "else", "/if"]:
|
||||
continue
|
||||
|
||||
bbcodes.append({
|
||||
bbcode = position.bbcode,
|
||||
start = position.start,
|
||||
offset_start = position.start - accumulaive_length_offset
|
||||
})
|
||||
accumulaive_length_offset += position.bbcode.length()
|
||||
|
||||
for bb in bbcodes:
|
||||
text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length())
|
||||
|
||||
# Now find any dialogue markers
|
||||
var next_bbcode_position = find_bbcode_positions_in_string(text, false)
|
||||
var limit = 0
|
||||
while next_bbcode_position.size() > 0 and limit < 1000:
|
||||
limit += 1
|
||||
|
||||
var bbcode = next_bbcode_position[0]
|
||||
var index = bbcode.start
|
||||
var code = bbcode.code
|
||||
var raw_args = bbcode.raw_args
|
||||
var args = {}
|
||||
if code in ["$>", "$>>", "do", "do!", "set"]:
|
||||
args["value"] = DMCompiler.extract_mutation("%s %s" % [code, raw_args])
|
||||
else:
|
||||
# Could be something like:
|
||||
# "=1.0"
|
||||
# " rate=20 level=10"
|
||||
if raw_args and raw_args[0] == "=":
|
||||
raw_args = "value" + raw_args
|
||||
for pair in raw_args.strip_edges().split(" "):
|
||||
if "=" in pair:
|
||||
var bits = pair.split("=")
|
||||
args[bits[0]] = bits[1]
|
||||
|
||||
match code:
|
||||
"wait":
|
||||
var wait: Dictionary = DMCompiler.extract_mutation("do wait(%s)" % [args.get("value", "null")])
|
||||
mutations.append([index, wait])
|
||||
"speed":
|
||||
speeds[index] = args.get("value").to_float()
|
||||
"/speed":
|
||||
speeds[index] = 1.0
|
||||
"$>", "$>>", "do", "do!", "set":
|
||||
mutations.append([index, args.get("value")])
|
||||
"next":
|
||||
time = args.get("value") if args.has("value") else "0"
|
||||
|
||||
# Find any BB codes that are after this index and remove the length from their start
|
||||
var length = bbcode.bbcode.length()
|
||||
for bb in bbcodes:
|
||||
if bb.offset_start > bbcode.start:
|
||||
bb.offset_start -= length
|
||||
bb.start -= length
|
||||
|
||||
# Find any escaped brackets after this that need moving
|
||||
for i in range(0, escaped_open_brackets.size()):
|
||||
if escaped_open_brackets[i] > bbcode.start:
|
||||
escaped_open_brackets[i] -= length
|
||||
for i in range(0, escaped_close_brackets.size()):
|
||||
if escaped_close_brackets[i] > bbcode.start:
|
||||
escaped_close_brackets[i] -= length
|
||||
|
||||
text = text.substr(0, index) + text.substr(index + length)
|
||||
next_bbcode_position = find_bbcode_positions_in_string(text, false)
|
||||
|
||||
# Put the BB Codes back in
|
||||
for bb in bbcodes:
|
||||
text = text.insert(bb.start, bb.bbcode)
|
||||
|
||||
# Put the escaped brackets back in
|
||||
for index in escaped_open_brackets:
|
||||
text = text.left(index) + "[" + text.right(text.length() - index - 1)
|
||||
for index in escaped_close_brackets:
|
||||
text = text.left(index) + "]" + text.right(text.length() - index - 1)
|
||||
|
||||
|
||||
func find_bbcode_positions_in_string(string: String, find_all: bool = true, include_conditions: bool = false) -> Array[Dictionary]:
|
||||
if not "[" in string: return []
|
||||
|
||||
var positions: Array[Dictionary] = []
|
||||
|
||||
var open_brace_count: int = 0
|
||||
var start: int = 0
|
||||
var bbcode: String = ""
|
||||
var code: String = ""
|
||||
var is_finished_code: bool = false
|
||||
for i in range(0, string.length()):
|
||||
if string[i] == "[":
|
||||
if open_brace_count == 0:
|
||||
start = i
|
||||
bbcode = ""
|
||||
code = ""
|
||||
is_finished_code = false
|
||||
open_brace_count += 1
|
||||
|
||||
else:
|
||||
if not is_finished_code and (string[i].to_upper() != string[i] or ["/", "!", "$", ">"].has(string[i])):
|
||||
code += string[i]
|
||||
else:
|
||||
is_finished_code = true
|
||||
|
||||
if open_brace_count > 0:
|
||||
bbcode += string[i]
|
||||
|
||||
if string[i] == "]":
|
||||
open_brace_count -= 1
|
||||
if open_brace_count == 0 and (include_conditions or not code in ["if", "else", "/if"]):
|
||||
positions.append({
|
||||
bbcode = bbcode,
|
||||
code = code,
|
||||
start = start,
|
||||
end = i,
|
||||
raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges()
|
||||
})
|
||||
|
||||
if not find_all:
|
||||
return positions
|
||||
|
||||
return positions
|
||||
@@ -0,0 +1 @@
|
||||
uid://0k6q8kukq0qa
|
||||
@@ -0,0 +1,26 @@
|
||||
## Tag data associated with a line of dialogue.
|
||||
class_name DMResolvedTagData extends RefCounted
|
||||
|
||||
|
||||
## The list of tags.
|
||||
var tags: PackedStringArray = []
|
||||
## The line with any tag syntax removed.
|
||||
var text_without_tags: String = ""
|
||||
|
||||
# An instance of the compiler [RegEx].
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
|
||||
|
||||
func _init(text: String) -> void:
|
||||
var resolved_tags: PackedStringArray = []
|
||||
var tag_matches: Array[RegExMatch] = regex.TAGS_REGEX.search_all(text)
|
||||
for tag_match in tag_matches:
|
||||
text = text.replace(tag_match.get_string(), "")
|
||||
var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",")
|
||||
for tag in tags:
|
||||
tag = tag.replace("#", "")
|
||||
if not tag in resolved_tags:
|
||||
resolved_tags.append(tag)
|
||||
|
||||
tags = resolved_tags
|
||||
text_without_tags = text
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqai3ikuilqfq
|
||||
@@ -0,0 +1,46 @@
|
||||
## An intermediate representation of a dialogue line before it gets compiled.
|
||||
class_name DMTreeLine extends RefCounted
|
||||
|
||||
|
||||
## The line number where this dialogue was found (after imported files have had their content imported).
|
||||
var line_number: int = 0
|
||||
## The parent [DMTreeLine] of this line.
|
||||
## This is stored as a Weak Reference so that this RefCounted can elegantly free itself.
|
||||
## Without it being a Weak Reference, this can easily cause a cyclical reference that keeps this resource alive.
|
||||
var parent: WeakRef
|
||||
## The ID of this line.
|
||||
var id: String
|
||||
## The type of this line (as a [String] defined in [DMConstants].
|
||||
var type: String = ""
|
||||
## Is this line part of a randomised group?
|
||||
var is_random: bool = false
|
||||
## The indent count for this line.
|
||||
var indent: int = 0
|
||||
## The text of this line.
|
||||
var text: String = ""
|
||||
## The child [DMTreeLine]s of this line.
|
||||
var children: Array[DMTreeLine] = []
|
||||
## Any doc comments attached to this line.
|
||||
var notes: String = ""
|
||||
## Is this a dialogue line that is the child of another dialogue line?
|
||||
var is_nested_dialogue: bool = false
|
||||
|
||||
|
||||
func _init(initial_id: String) -> void:
|
||||
id = initial_id
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
var tabs = []
|
||||
tabs.resize(indent)
|
||||
tabs.fill("\t")
|
||||
tabs = "".join(tabs)
|
||||
|
||||
return tabs.join([tabs + "{\n",
|
||||
"\tid: %s\n" % [id],
|
||||
"\ttype: %s\n" % [type],
|
||||
"\tis_random: %s\n" % ["true" if is_random else "false"],
|
||||
"\ttext: %s\n" % [text],
|
||||
"\tnotes: %s\n" % [notes],
|
||||
"\tchildren: []\n" if children.size() == 0 else "\tchildren: [\n" + ",\n".join(children.map(func(child): return str(child))) + "]\n",
|
||||
"}"])
|
||||
@@ -0,0 +1 @@
|
||||
uid://dsu4i84dpif14
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://djeybvlb332mp
|
||||
@@ -0,0 +1,56 @@
|
||||
[gd_scene format=3 uid="uid://civ6shmka5e8u"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"]
|
||||
[ext_resource type="Script" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"]
|
||||
|
||||
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_crbvo"]
|
||||
script = ExtResource("1_58cfo")
|
||||
|
||||
[node name="CodeEdit" type="CodeEdit" unique_id=236286673]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
text = "~ title_thing
|
||||
|
||||
if this = \"that\" or 'this'
|
||||
Nathan: Something
|
||||
- Then [if test.thing() == 2.0] => somewhere
|
||||
- Other => END!
|
||||
|
||||
~ somewhere
|
||||
|
||||
set has_something = true
|
||||
=> END"
|
||||
scroll_past_end_of_file = true
|
||||
minimap_draw = true
|
||||
syntax_highlighter = SubResource("SyntaxHighlighter_crbvo")
|
||||
highlight_all_occurrences = true
|
||||
highlight_current_line = true
|
||||
draw_tabs = true
|
||||
symbol_lookup_on_click = true
|
||||
line_folding = true
|
||||
gutters_draw_line_numbers = true
|
||||
gutters_draw_fold_gutter = true
|
||||
delimiter_strings = Array[String](["\" \""])
|
||||
delimiter_comments = Array[String](["#"])
|
||||
code_completion_enabled = true
|
||||
code_completion_prefixes = Array[String]([">", "<"])
|
||||
indent_automatic = true
|
||||
auto_brace_completion_enabled = true
|
||||
auto_brace_completion_highlight_matching = true
|
||||
auto_brace_completion_pairs = {
|
||||
"\"": "\"",
|
||||
"(": ")",
|
||||
"[": "]",
|
||||
"{": "}"
|
||||
}
|
||||
script = ExtResource("1_g324i")
|
||||
|
||||
[connection signal="caret_changed" from="." to="." method="_on_code_edit_caret_changed"]
|
||||
[connection signal="gutter_clicked" from="." to="." method="_on_code_edit_gutter_clicked"]
|
||||
[connection signal="symbol_lookup" from="." to="." method="_on_code_edit_symbol_lookup"]
|
||||
[connection signal="symbol_validate" from="." to="." method="_on_code_edit_symbol_validate"]
|
||||
[connection signal="text_changed" from="." to="." method="_on_code_edit_text_changed"]
|
||||
[connection signal="text_set" from="." to="." method="_on_code_edit_text_set"]
|
||||
@@ -0,0 +1,237 @@
|
||||
@tool
|
||||
class_name DMSyntaxHighlighter extends SyntaxHighlighter
|
||||
|
||||
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
var expression_parser = DMExpressionParser.new()
|
||||
|
||||
var cache: Dictionary = {}
|
||||
|
||||
|
||||
func _clear_highlighting_cache() -> void:
|
||||
cache.clear()
|
||||
|
||||
|
||||
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||
expression_parser.include_comments = true
|
||||
|
||||
var colors: Dictionary = {}
|
||||
var text_edit: TextEdit = get_text_edit()
|
||||
var text: String = text_edit.get_line(line)
|
||||
|
||||
# Prevent an error from popping up while developing
|
||||
if not is_instance_valid(text_edit) or text_edit.theme_overrides.is_empty():
|
||||
return colors
|
||||
|
||||
# Disable this, as well as the line at the bottom of this function to remove the cache.
|
||||
if text in cache:
|
||||
return cache[text]
|
||||
|
||||
var theme: Dictionary = text_edit.theme_overrides
|
||||
|
||||
var index: int = 0
|
||||
|
||||
match DMCompiler.get_line_type(text):
|
||||
DMConstants.TYPE_USING:
|
||||
colors[index] = { color = theme.conditions_color }
|
||||
colors[index + "using ".length()] = { color = theme.text_color }
|
||||
|
||||
DMConstants.TYPE_IMPORT:
|
||||
colors[index] = { color = theme.conditions_color }
|
||||
var import: RegExMatch = regex.IMPORT_REGEX.search(text)
|
||||
if import:
|
||||
colors[index + import.get_start("path") - 1] = { color = theme.strings_color }
|
||||
colors[index + import.get_end("path") + 1] = { color = theme.conditions_color }
|
||||
colors[index + import.get_start("prefix")] = { color = theme.text_color }
|
||||
|
||||
DMConstants.TYPE_COMMENT:
|
||||
colors[index] = { color = theme.comments_color }
|
||||
|
||||
DMConstants.TYPE_TITLE:
|
||||
colors[index] = { color = theme.titles_color }
|
||||
|
||||
DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN:
|
||||
colors[0] = { color = theme.conditions_color }
|
||||
index = text.find(" ")
|
||||
if index > -1:
|
||||
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0)
|
||||
if expression.size() == 0:
|
||||
colors[index] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index)
|
||||
|
||||
DMConstants.TYPE_MUTATION:
|
||||
colors[0] = { color = theme.mutations_line_color }
|
||||
index = text.find(" ")
|
||||
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0)
|
||||
if expression.size() == 0:
|
||||
colors[index] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index)
|
||||
|
||||
DMConstants.TYPE_GOTO:
|
||||
if text.strip_edges().begins_with("%"):
|
||||
colors[index] = { color = theme.symbols_color }
|
||||
index = text.find(" ")
|
||||
_highlight_goto(text, colors, index)
|
||||
|
||||
DMConstants.TYPE_RANDOM:
|
||||
colors[index] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE:
|
||||
if text.strip_edges().begins_with("%"):
|
||||
colors[index] = { color = theme.symbols_color }
|
||||
index = text.find(" ", text.find("%"))
|
||||
colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) }
|
||||
|
||||
var dialogue_text: String = text.substr(index, text.find("=>"))
|
||||
|
||||
# Highlight character name (but ignore ":" within line ID reference)
|
||||
var split_index: int = dialogue_text.replace("\\:", "??").find(":")
|
||||
if text.substr(split_index - 3, 3) != "[ID":
|
||||
colors[index + split_index + 1] = { color = theme.text_color }
|
||||
else:
|
||||
# If there's no character name then just highlight the text as dialogue.
|
||||
colors[index] = { color = theme.text_color }
|
||||
|
||||
# Interpolation
|
||||
var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text)
|
||||
for replacement: RegExMatch in replacements:
|
||||
var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2)
|
||||
var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start())
|
||||
var expression_index: int = index + replacement.get_start()
|
||||
colors[expression_index] = { color = theme.symbols_color }
|
||||
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
|
||||
colors[expression_index] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index + 2)
|
||||
colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color }
|
||||
colors[expression_index + expression_text.length() + 4] = { color = theme.text_color }
|
||||
# Tags (and inline mutations)
|
||||
var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("")
|
||||
var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true)
|
||||
for bbcode: Dictionary in bbcodes:
|
||||
var tag: String = bbcode.code
|
||||
var code: String = bbcode.raw_args
|
||||
if code.begins_with("["):
|
||||
colors[index + bbcode.start] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.start + 2] = { color = theme.text_color }
|
||||
var pipe_cursor: int = code.find("|")
|
||||
while pipe_cursor > -1:
|
||||
colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color }
|
||||
pipe_cursor = code.find("|", pipe_cursor + 1)
|
||||
colors[index + bbcode.end - 1] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.end + 1] = { color = theme.text_color }
|
||||
else:
|
||||
colors[index + bbcode.start] = { color = theme.symbols_color }
|
||||
if tag.begins_with("$>") or tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"):
|
||||
if tag.begins_with("if"):
|
||||
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
|
||||
else:
|
||||
colors[index + bbcode.start + 1] = { color = theme.mutations_line_color }
|
||||
var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length())
|
||||
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
|
||||
colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index + 2)
|
||||
# else and closing if have no expression
|
||||
elif tag.begins_with("else") or tag.begins_with("/if"):
|
||||
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
|
||||
colors[index + bbcode.end] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.end + 1] = { color = theme.text_color }
|
||||
# Jumps
|
||||
if "=> " in text or "=>< " in text:
|
||||
_highlight_goto(text, colors, index)
|
||||
|
||||
# Order the dictionary keys to prevent CodeEdit from having issues
|
||||
var ordered_colors: Dictionary = {}
|
||||
var ordered_keys: Array = colors.keys()
|
||||
ordered_keys.sort()
|
||||
for key_index: int in ordered_keys:
|
||||
ordered_colors[key_index] = colors[key_index]
|
||||
|
||||
cache[text] = ordered_colors
|
||||
return ordered_colors
|
||||
|
||||
|
||||
func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int:
|
||||
var theme: Dictionary = get_text_edit().theme_overrides
|
||||
var last_index: int = index
|
||||
for token: Dictionary in tokens:
|
||||
last_index = token.i
|
||||
match token.type:
|
||||
DMConstants.TOKEN_COMMENT:
|
||||
colors[index + token.i] = { color = theme.comments_color }
|
||||
|
||||
DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR, DMConstants.TOKEN_NOT:
|
||||
colors[index + token.i] = { color = theme.conditions_color }
|
||||
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
if token.value in ["true", "false"]:
|
||||
colors[index + token.i] = { color = theme.conditions_color }
|
||||
else:
|
||||
colors[index + token.i] = { color = theme.members_color }
|
||||
|
||||
DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \
|
||||
DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \
|
||||
DMConstants.TOKEN_ASSIGNMENT, DMConstants.TOKEN_COMPARISON:
|
||||
if token.get("value", null) == "in":
|
||||
colors[index + token.i] = { color = theme.conditions_color }
|
||||
else:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TOKEN_NUMBER:
|
||||
colors[index + token.i] = { color = theme.numbers_color }
|
||||
|
||||
DMConstants.TOKEN_STRING:
|
||||
colors[index + token.i] = { color = theme.strings_color }
|
||||
|
||||
DMConstants.TOKEN_FUNCTION:
|
||||
colors[index + token.i] = { color = theme.mutations_color }
|
||||
colors[index + token.i + token.function.length()] = { color = theme.symbols_color }
|
||||
for parameter: Array in token.value:
|
||||
last_index = _highlight_expression(parameter, colors, index)
|
||||
DMConstants.TOKEN_PARENS_CLOSE:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||
colors[index + token.i] = { color = theme.members_color }
|
||||
colors[index + token.i + token.variable.length()] = { color = theme.symbols_color }
|
||||
last_index = _highlight_expression(token.value, colors, index)
|
||||
DMConstants.TOKEN_ARRAY:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
for item: Array in token.value:
|
||||
last_index = _highlight_expression(item, colors, index)
|
||||
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TOKEN_DICTIONARY:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index)
|
||||
DMConstants.TOKEN_BRACE_CLOSE:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
last_index += 1
|
||||
|
||||
DMConstants.TOKEN_GROUP:
|
||||
last_index = _highlight_expression(token.value, colors, index)
|
||||
|
||||
return last_index
|
||||
|
||||
|
||||
func _highlight_goto(text: String, colors: Dictionary, index: int) -> int:
|
||||
var theme: Dictionary = get_text_edit().theme_overrides
|
||||
var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {})
|
||||
colors[goto_data.index] = { color = theme.jumps_color }
|
||||
if "{{" in text:
|
||||
index = text.find("{{", goto_data.index)
|
||||
var last_index: int = 0
|
||||
if goto_data.error:
|
||||
colors[index + 2] = { color = theme.critical_color }
|
||||
else:
|
||||
last_index = _highlight_expression(goto_data.expression, colors, index)
|
||||
index = text.find("}}", index + last_index)
|
||||
colors[index] = { color = theme.jumps_color }
|
||||
|
||||
return index
|
||||
@@ -0,0 +1 @@
|
||||
uid://klpiq4tk3t7a
|
||||
@@ -0,0 +1,84 @@
|
||||
@tool
|
||||
extends Control
|
||||
|
||||
|
||||
signal failed()
|
||||
signal updated(updated_to_version: String)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
const TEMP_FILE_NAME = "user://temp.zip"
|
||||
|
||||
|
||||
@onready var logo: TextureRect = %Logo
|
||||
@onready var label: Label = $VBox/Label
|
||||
@onready var http_request: HTTPRequest = $HTTPRequest
|
||||
@onready var download_button: Button = %DownloadButton
|
||||
|
||||
var next_version_release: Dictionary:
|
||||
set(value):
|
||||
next_version_release = value
|
||||
label.text = DialogueConstants.translate(&"update.is_available_for_download") % value.tag_name.substr(1)
|
||||
get:
|
||||
return next_version_release
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
$VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update")
|
||||
$VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes")
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_download_button_pressed() -> void:
|
||||
# Safeguard the actual dialogue manager repo from accidentally updating itself
|
||||
if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"):
|
||||
prints("You can't update the addon from within itself.")
|
||||
failed.emit()
|
||||
return
|
||||
|
||||
http_request.request(next_version_release.zipball_url)
|
||||
download_button.disabled = true
|
||||
download_button.text = DialogueConstants.translate(&"update.downloading")
|
||||
|
||||
|
||||
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
failed.emit()
|
||||
return
|
||||
|
||||
# Save the downloaded zip
|
||||
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
|
||||
zip_file.store_buffer(body)
|
||||
zip_file.close()
|
||||
|
||||
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager"))
|
||||
|
||||
var zip_reader: ZIPReader = ZIPReader.new()
|
||||
zip_reader.open(TEMP_FILE_NAME)
|
||||
var files: PackedStringArray = zip_reader.get_files()
|
||||
|
||||
var base_path = files[1]
|
||||
# Remove archive folder
|
||||
files.remove_at(0)
|
||||
# Remove assets folder
|
||||
files.remove_at(0)
|
||||
|
||||
for path in files:
|
||||
var new_file_path: String = path.replace(base_path, "")
|
||||
if path.ends_with("/"):
|
||||
DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path)
|
||||
else:
|
||||
var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE)
|
||||
file.store_buffer(zip_reader.read_file(path))
|
||||
|
||||
zip_reader.close()
|
||||
DirAccess.remove_absolute(TEMP_FILE_NAME)
|
||||
|
||||
updated.emit(next_version_release.tag_name.substr(1))
|
||||
|
||||
|
||||
func _on_notes_button_pressed() -> void:
|
||||
OS.shell_open(next_version_release.html_url)
|
||||
@@ -0,0 +1 @@
|
||||
uid://kpwo418lb2t2
|
||||
@@ -0,0 +1,60 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://kpwo418lb2t2" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"]
|
||||
[ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"]
|
||||
|
||||
[node name="DownloadUpdatePanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_4tm1k")
|
||||
|
||||
[node name="HTTPRequest" type="HTTPRequest" parent="."]
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -1.0
|
||||
offset_top = 9.0
|
||||
offset_right = -1.0
|
||||
offset_bottom = 9.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Logo" type="TextureRect" parent="VBox"]
|
||||
unique_name_in_owner = true
|
||||
clip_contents = true
|
||||
custom_minimum_size = Vector2(300, 80)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("2_4o2m6")
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="Label" type="Label" parent="VBox"]
|
||||
layout_mode = 2
|
||||
text = "v1.2.3 is available for download."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Center" type="CenterContainer" parent="VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DownloadButton" type="Button" parent="VBox/Center"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Download update"
|
||||
|
||||
[node name="Center2" type="CenterContainer" parent="VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="NotesButton" type="LinkButton" parent="VBox/Center2"]
|
||||
layout_mode = 2
|
||||
text = "Read release notes"
|
||||
|
||||
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
|
||||
[connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"]
|
||||
[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"]
|
||||
@@ -0,0 +1,45 @@
|
||||
@tool
|
||||
|
||||
class_name DMDialogueEditorProperty extends EditorProperty
|
||||
|
||||
|
||||
const DialoguePropertyEditorControl: PackedScene = preload("./editor_property_control.tscn")
|
||||
|
||||
|
||||
var control = DialoguePropertyEditorControl.instantiate()
|
||||
var current_value: DialogueResource
|
||||
var is_updating: bool = false
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
add_child(control)
|
||||
|
||||
control.resource = current_value
|
||||
|
||||
control.resource_changed.connect(_on_resource_changed)
|
||||
|
||||
|
||||
func _update_property() -> void:
|
||||
var next_value: DialogueResource = get_edited_object()[get_edited_property()]
|
||||
|
||||
# The resource might have been deleted elsewhere so check that it's not in a weird state
|
||||
if is_instance_valid(next_value) and not next_value.resource_path.ends_with(".dialogue"):
|
||||
emit_changed(get_edited_property(), null)
|
||||
return
|
||||
|
||||
if next_value == current_value: return
|
||||
|
||||
is_updating = true
|
||||
current_value = next_value
|
||||
control.resource = current_value
|
||||
is_updating = false
|
||||
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_resource_changed(next_resource: DialogueResource) -> void:
|
||||
emit_changed(get_edited_property(), next_resource)
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://nyypeje1a036
|
||||
@@ -0,0 +1,153 @@
|
||||
@tool
|
||||
|
||||
extends HBoxContainer
|
||||
|
||||
|
||||
signal pressed()
|
||||
signal resource_changed(next_resource: DialogueResource)
|
||||
|
||||
|
||||
const ITEM_NEW: int = 100
|
||||
const ITEM_QUICK_LOAD: int = 200
|
||||
const ITEM_LOAD: int = 201
|
||||
const ITEM_EDIT: int = 300
|
||||
const ITEM_CLEAR: int = 301
|
||||
const ITEM_FILESYSTEM: int = 400
|
||||
|
||||
|
||||
@onready var button: Button = $ResourceButton
|
||||
@onready var menu_button: Button = $MenuButton
|
||||
@onready var menu: PopupMenu = $Menu
|
||||
@onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog
|
||||
@onready var files_list = $QuickOpenDialog/FilesList
|
||||
@onready var new_dialog: FileDialog = $NewDialog
|
||||
@onready var open_dialog: FileDialog = $OpenDialog
|
||||
|
||||
var resource: Resource:
|
||||
set(next_resource):
|
||||
resource = next_resource
|
||||
if button:
|
||||
button.resource = resource
|
||||
get:
|
||||
return resource
|
||||
|
||||
var is_waiting_for_file: bool = false
|
||||
var quick_selected_file: String = ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
menu_button.icon = get_theme_icon("GuiDropdown", "EditorIcons")
|
||||
|
||||
|
||||
func build_menu() -> void:
|
||||
menu.clear()
|
||||
|
||||
menu.add_icon_item(DMPlugin.instance._get_plugin_icon(), "New Dialogue", ITEM_NEW)
|
||||
menu.add_separator()
|
||||
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Quick Load", ITEM_QUICK_LOAD)
|
||||
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Load", ITEM_LOAD)
|
||||
if resource:
|
||||
menu.add_icon_item(get_theme_icon("Edit", "EditorIcons"), "Edit", ITEM_EDIT)
|
||||
menu.add_icon_item(get_theme_icon("Clear", "EditorIcons"), "Clear", ITEM_CLEAR)
|
||||
menu.add_separator()
|
||||
menu.add_item("Show in FileSystem", ITEM_FILESYSTEM)
|
||||
|
||||
menu.size = Vector2.ZERO
|
||||
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_new_dialog_file_selected(path: String) -> void:
|
||||
DMPlugin.instance.main_view.new_file(path)
|
||||
is_waiting_for_file = false
|
||||
if DMCache.has_file(path):
|
||||
resource_changed.emit(load(path))
|
||||
else:
|
||||
var next_resource: DialogueResource = await DMPlugin.instance.import_plugin.compiled_resource
|
||||
next_resource.resource_path = path
|
||||
resource_changed.emit(next_resource)
|
||||
|
||||
|
||||
func _on_open_dialog_file_selected(file: String) -> void:
|
||||
resource_changed.emit(load(file))
|
||||
|
||||
|
||||
func _on_file_dialog_canceled() -> void:
|
||||
is_waiting_for_file = false
|
||||
|
||||
|
||||
func _on_resource_button_pressed() -> void:
|
||||
if is_instance_valid(resource):
|
||||
EditorInterface.call_deferred("edit_resource", resource)
|
||||
|
||||
elif menu.visible:
|
||||
menu.hide()
|
||||
else:
|
||||
build_menu()
|
||||
menu.position = get_viewport().position + Vector2i(
|
||||
button.global_position.x + button.size.x - menu.size.x,
|
||||
2 + menu_button.global_position.y + button.size.y
|
||||
)
|
||||
menu.popup()
|
||||
|
||||
|
||||
func _on_resource_button_resource_dropped(next_resource: Resource) -> void:
|
||||
resource_changed.emit(next_resource)
|
||||
|
||||
|
||||
func _on_menu_button_pressed() -> void:
|
||||
if menu.visible:
|
||||
menu.hide()
|
||||
else:
|
||||
build_menu()
|
||||
menu.position = get_viewport().position + Vector2i(
|
||||
menu_button.global_position.x + menu_button.size.x - menu.size.x,
|
||||
2 + menu_button.global_position.y + menu_button.size.y
|
||||
)
|
||||
menu.popup()
|
||||
|
||||
|
||||
func _on_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
ITEM_NEW:
|
||||
is_waiting_for_file = true
|
||||
new_dialog.popup_centered()
|
||||
|
||||
ITEM_QUICK_LOAD:
|
||||
quick_selected_file = ""
|
||||
files_list.files = DMCache.get_files()
|
||||
if resource:
|
||||
files_list.select_file(resource.resource_path)
|
||||
quick_open_dialog.popup_centered()
|
||||
files_list.focus_filter()
|
||||
|
||||
ITEM_LOAD:
|
||||
is_waiting_for_file = true
|
||||
open_dialog.popup_centered()
|
||||
|
||||
ITEM_EDIT:
|
||||
EditorInterface.call_deferred("edit_resource", resource)
|
||||
|
||||
ITEM_CLEAR:
|
||||
resource_changed.emit(null)
|
||||
|
||||
ITEM_FILESYSTEM:
|
||||
EditorInterface.get_file_system_dock().navigate_to_path(resource.resource_path)
|
||||
|
||||
|
||||
func _on_files_list_file_double_clicked(file_path: String) -> void:
|
||||
resource_changed.emit(load(file_path))
|
||||
quick_open_dialog.hide()
|
||||
|
||||
|
||||
func _on_files_list_file_selected(file_path: String) -> void:
|
||||
quick_selected_file = file_path
|
||||
|
||||
|
||||
func _on_quick_open_dialog_confirmed() -> void:
|
||||
if quick_selected_file != "":
|
||||
resource_changed.emit(load(quick_selected_file))
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://dooe2pflnqtve
|
||||
@@ -0,0 +1,58 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dooe2pflnqtve" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"]
|
||||
[ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"]
|
||||
[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"]
|
||||
|
||||
[node name="PropertyEditorButton" type="HBoxContainer"]
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 0
|
||||
script = ExtResource("1_het12")
|
||||
|
||||
[node name="ResourceButton" parent="." instance=ExtResource("2_hh3d4")]
|
||||
layout_mode = 2
|
||||
text = "<empty>"
|
||||
text_overrun_behavior = 3
|
||||
clip_text = true
|
||||
|
||||
[node name="MenuButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Menu" type="PopupMenu" parent="."]
|
||||
|
||||
[node name="QuickOpenDialog" type="ConfirmationDialog" parent="."]
|
||||
title = "Find Dialogue Resource"
|
||||
size = Vector2i(400, 600)
|
||||
min_size = Vector2i(400, 600)
|
||||
ok_button_text = "Open"
|
||||
|
||||
[node name="FilesList" parent="QuickOpenDialog" instance=ExtResource("3_l8fp6")]
|
||||
|
||||
[node name="NewDialog" type="FileDialog" parent="."]
|
||||
size = Vector2i(900, 750)
|
||||
min_size = Vector2i(900, 750)
|
||||
dialog_hide_on_ok = true
|
||||
filters = PackedStringArray("*.dialogue ; Dialogue")
|
||||
|
||||
[node name="OpenDialog" type="FileDialog" parent="."]
|
||||
title = "Open a File"
|
||||
size = Vector2i(900, 750)
|
||||
min_size = Vector2i(900, 750)
|
||||
ok_button_text = "Open"
|
||||
dialog_hide_on_ok = true
|
||||
file_mode = 0
|
||||
filters = PackedStringArray("*.dialogue ; Dialogue")
|
||||
|
||||
[connection signal="pressed" from="ResourceButton" to="." method="_on_resource_button_pressed"]
|
||||
[connection signal="resource_dropped" from="ResourceButton" to="." method="_on_resource_button_resource_dropped"]
|
||||
[connection signal="pressed" from="MenuButton" to="." method="_on_menu_button_pressed"]
|
||||
[connection signal="id_pressed" from="Menu" to="." method="_on_menu_id_pressed"]
|
||||
[connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"]
|
||||
[connection signal="file_double_clicked" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_double_clicked"]
|
||||
[connection signal="file_selected" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_selected"]
|
||||
[connection signal="canceled" from="NewDialog" to="." method="_on_file_dialog_canceled"]
|
||||
[connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"]
|
||||
[connection signal="canceled" from="OpenDialog" to="." method="_on_file_dialog_canceled"]
|
||||
[connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"]
|
||||
@@ -0,0 +1,48 @@
|
||||
@tool
|
||||
extends Button
|
||||
|
||||
|
||||
signal resource_dropped(next_resource: Resource)
|
||||
|
||||
|
||||
var resource: Resource:
|
||||
set(next_resource):
|
||||
resource = next_resource
|
||||
if resource:
|
||||
icon = DMPlugin.instance._get_plugin_icon()
|
||||
text = resource.resource_path.get_file().replace(".dialogue", "")
|
||||
else:
|
||||
icon = null
|
||||
text = "<empty>"
|
||||
get:
|
||||
return resource
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
match what:
|
||||
NOTIFICATION_DRAG_BEGIN:
|
||||
var data = get_viewport().gui_get_drag_data()
|
||||
if typeof(data) == TYPE_DICTIONARY and data.type == "files" and data.files.size() > 0 and data.files[0].ends_with(".dialogue"):
|
||||
add_theme_stylebox_override("normal", get_theme_stylebox("focus", "LineEdit"))
|
||||
add_theme_stylebox_override("hover", get_theme_stylebox("focus", "LineEdit"))
|
||||
|
||||
NOTIFICATION_DRAG_END:
|
||||
self.resource = resource
|
||||
remove_theme_stylebox_override("normal")
|
||||
remove_theme_stylebox_override("hover")
|
||||
|
||||
|
||||
func _can_drop_data(at_position: Vector2, data) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY: return false
|
||||
if data.type != "files": return false
|
||||
|
||||
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
|
||||
return files.size() > 0
|
||||
|
||||
|
||||
func _drop_data(at_position: Vector2, data) -> void:
|
||||
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
|
||||
|
||||
if files.size() == 0: return
|
||||
|
||||
resource_dropped.emit(load(files[0]))
|
||||
@@ -0,0 +1 @@
|
||||
uid://damhqta55t67c
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://damhqta55t67c" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"]
|
||||
|
||||
[node name="ResourceButton" type="Button"]
|
||||
offset_right = 8.0
|
||||
offset_bottom = 8.0
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_7u2i7")
|
||||
@@ -0,0 +1,85 @@
|
||||
@tool
|
||||
extends HBoxContainer
|
||||
|
||||
|
||||
signal error_pressed(line_number)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
|
||||
@onready var error_button: Button = $ErrorButton
|
||||
@onready var next_button: Button = $NextButton
|
||||
@onready var count_label: Label = $CountLabel
|
||||
@onready var previous_button: Button = $PreviousButton
|
||||
|
||||
## The index of the current error being shown
|
||||
var error_index: int = 0:
|
||||
set(next_error_index):
|
||||
error_index = wrap(next_error_index, 0, errors.size())
|
||||
show_error()
|
||||
get:
|
||||
return error_index
|
||||
|
||||
## The list of all errors
|
||||
var errors: Array = []:
|
||||
set(next_errors):
|
||||
errors = next_errors
|
||||
self.error_index = 0
|
||||
get:
|
||||
return errors
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
hide()
|
||||
|
||||
|
||||
## Set up colors and icons
|
||||
func apply_theme() -> void:
|
||||
error_button.add_theme_color_override("font_color", get_theme_color("error_color", "Editor"))
|
||||
error_button.add_theme_color_override("font_hover_color", get_theme_color("error_color", "Editor"))
|
||||
error_button.icon = get_theme_icon("StatusError", "EditorIcons")
|
||||
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
|
||||
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
|
||||
|
||||
|
||||
## Move the error index to match a given line
|
||||
func show_error_for_line_number(line_number: int) -> void:
|
||||
for i in range(0, errors.size()):
|
||||
if errors[i].line_number == line_number:
|
||||
self.error_index = i
|
||||
|
||||
|
||||
## Show the current error
|
||||
func show_error() -> void:
|
||||
if errors.size() == 0:
|
||||
hide()
|
||||
else:
|
||||
show()
|
||||
count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() })
|
||||
var error = errors[error_index]
|
||||
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
|
||||
if error.has("external_error"):
|
||||
error_button.text += " " + DialogueConstants.get_error_message(error.external_error)
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_errors_panel_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_error_button_pressed() -> void:
|
||||
error_pressed.emit(errors[error_index].line_number, errors[error_index].column_number)
|
||||
|
||||
|
||||
func _on_previous_button_pressed() -> void:
|
||||
self.error_index -= 1
|
||||
_on_error_button_pressed()
|
||||
|
||||
|
||||
func _on_next_button_pressed() -> void:
|
||||
self.error_index += 1
|
||||
_on_error_button_pressed()
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2l8nlb6hhrfp
|
||||
@@ -0,0 +1,56 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"]
|
||||
|
||||
[sub_resource type="Image" id="Image_w0gko"]
|
||||
data = {
|
||||
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"]
|
||||
image = SubResource("Image_w0gko")
|
||||
|
||||
[node name="ErrorsPanel" type="HBoxContainer"]
|
||||
visible = false
|
||||
offset_right = 1024.0
|
||||
offset_bottom = 600.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_nfm3c")
|
||||
metadata/_edit_layout_mode = 1
|
||||
|
||||
[node name="ErrorButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
|
||||
theme_override_constants/h_separation = 3
|
||||
icon = SubResource("ImageTexture_s6fxl")
|
||||
flat = true
|
||||
alignment = 0
|
||||
text_overrun_behavior = 4
|
||||
|
||||
[node name="Spacer" type="Control" parent="."]
|
||||
custom_minimum_size = Vector2(40, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PreviousButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
icon = SubResource("ImageTexture_s6fxl")
|
||||
flat = true
|
||||
|
||||
[node name="CountLabel" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="NextButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
icon = SubResource("ImageTexture_s6fxl")
|
||||
flat = true
|
||||
|
||||
[connection signal="pressed" from="ErrorButton" to="." method="_on_error_button_pressed"]
|
||||
[connection signal="pressed" from="PreviousButton" to="." method="_on_previous_button_pressed"]
|
||||
[connection signal="pressed" from="NextButton" to="." method="_on_next_button_pressed"]
|
||||
@@ -0,0 +1,150 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
|
||||
signal file_selected(file_path: String)
|
||||
signal file_popup_menu_requested(at_position: Vector2)
|
||||
signal file_double_clicked(file_path: String)
|
||||
signal file_middle_clicked(file_path: String)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
const MODIFIED_SUFFIX = "(*)"
|
||||
|
||||
|
||||
@export var icon: Texture2D
|
||||
|
||||
@onready var filter_edit: LineEdit = $FilterEdit
|
||||
@onready var list: ItemList = $List
|
||||
|
||||
var file_map: Dictionary = {}
|
||||
|
||||
var current_file_path: String = ""
|
||||
var last_selected_file_path: String = ""
|
||||
|
||||
var files: PackedStringArray = []:
|
||||
set(next_files):
|
||||
files = next_files
|
||||
files.sort()
|
||||
update_file_map()
|
||||
apply_filter()
|
||||
get:
|
||||
return files
|
||||
|
||||
var unsaved_files: Array[String] = []
|
||||
|
||||
var filter: String = "":
|
||||
set(next_filter):
|
||||
filter = next_filter
|
||||
apply_filter()
|
||||
get:
|
||||
return filter
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
|
||||
filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter")
|
||||
|
||||
|
||||
func focus_filter() -> void:
|
||||
filter_edit.grab_focus()
|
||||
|
||||
|
||||
func select_file(file: String) -> void:
|
||||
list.deselect_all()
|
||||
for i in range(0, list.get_item_count()):
|
||||
var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "")
|
||||
if item_text == get_nice_file(file, item_text.count("/") + 1):
|
||||
list.select(i)
|
||||
last_selected_file_path = file
|
||||
|
||||
|
||||
func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void:
|
||||
if not file in unsaved_files and is_unsaved:
|
||||
unsaved_files.append(file)
|
||||
elif file in unsaved_files and not is_unsaved:
|
||||
unsaved_files.erase(file)
|
||||
apply_filter()
|
||||
|
||||
|
||||
func update_file_map() -> void:
|
||||
file_map = {}
|
||||
for file in files:
|
||||
var nice_file: String = get_nice_file(file)
|
||||
|
||||
# See if a value with just the file name is already in the map
|
||||
for key in file_map.keys():
|
||||
if file_map[key] == nice_file:
|
||||
var bit_count = nice_file.count("/") + 2
|
||||
|
||||
var existing_nice_file = get_nice_file(key, bit_count)
|
||||
nice_file = get_nice_file(file, bit_count)
|
||||
|
||||
while nice_file == existing_nice_file:
|
||||
bit_count += 1
|
||||
existing_nice_file = get_nice_file(key, bit_count)
|
||||
nice_file = get_nice_file(file, bit_count)
|
||||
|
||||
file_map[key] = existing_nice_file
|
||||
|
||||
file_map[file] = nice_file
|
||||
|
||||
|
||||
func get_nice_file(file_path: String, path_bit_count: int = 1) -> String:
|
||||
var bits = file_path.replace("res://", "").replace(".dialogue", "").split("/")
|
||||
bits = bits.slice(-path_bit_count)
|
||||
return "/".join(bits)
|
||||
|
||||
|
||||
func apply_filter() -> void:
|
||||
list.clear()
|
||||
for file in file_map.keys():
|
||||
if filter == "" or filter.to_lower() in file.to_lower():
|
||||
var nice_file = file_map[file]
|
||||
if file in unsaved_files:
|
||||
nice_file += MODIFIED_SUFFIX
|
||||
var new_id := list.add_item(nice_file)
|
||||
list.set_item_icon(new_id, icon)
|
||||
|
||||
select_file(current_file_path)
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
if is_instance_valid(filter_edit):
|
||||
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||
if is_instance_valid(list):
|
||||
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_filter_edit_text_changed(new_text: String) -> void:
|
||||
self.filter = new_text
|
||||
|
||||
|
||||
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
|
||||
var file = file_map.find_key(item_text)
|
||||
|
||||
if mouse_button_index == MOUSE_BUTTON_LEFT or mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||
select_file(file)
|
||||
file_selected.emit(file)
|
||||
if mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||
file_popup_menu_requested.emit(at_position)
|
||||
|
||||
if mouse_button_index == MOUSE_BUTTON_MIDDLE:
|
||||
file_middle_clicked.emit(file)
|
||||
|
||||
|
||||
func _on_list_item_activated(index: int) -> void:
|
||||
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
|
||||
var file = file_map.find_key(item_text)
|
||||
select_file(file)
|
||||
file_double_clicked.emit(file)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dqa4a4wwoo0aa
|
||||
@@ -0,0 +1,29 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"]
|
||||
[ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"]
|
||||
|
||||
[node name="FilesList" type="VBoxContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_cytii")
|
||||
icon = ExtResource("2_3ijx1")
|
||||
|
||||
[node name="FilterEdit" type="LineEdit" parent="."]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Filter files"
|
||||
clear_button_enabled = true
|
||||
|
||||
[node name="List" type="ItemList" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
allow_rmb_select = true
|
||||
|
||||
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
|
||||
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
|
||||
[connection signal="item_activated" from="List" to="." method="_on_list_item_activated"]
|
||||
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]
|
||||
@@ -0,0 +1,222 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
|
||||
signal open_requested()
|
||||
signal close_requested()
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
|
||||
@onready var input: LineEdit = $Search/Input
|
||||
@onready var result_label: Label = $Search/ResultLabel
|
||||
@onready var previous_button: Button = $Search/PreviousButton
|
||||
@onready var next_button: Button = $Search/NextButton
|
||||
@onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox
|
||||
@onready var replace_check_button: CheckButton = $Search/ReplaceCheckButton
|
||||
@onready var replace_panel: HBoxContainer = $Replace
|
||||
@onready var replace_input: LineEdit = $Replace/Input
|
||||
@onready var replace_button: Button = $Replace/ReplaceButton
|
||||
@onready var replace_all_button: Button = $Replace/ReplaceAllButton
|
||||
|
||||
# The code edit we will be affecting (for some reason exporting this didn't work)
|
||||
var code_edit: CodeEdit:
|
||||
set(next_code_edit):
|
||||
code_edit = next_code_edit
|
||||
code_edit.gui_input.connect(_on_text_edit_gui_input)
|
||||
code_edit.text_changed.connect(_on_text_edit_text_changed)
|
||||
get:
|
||||
return code_edit
|
||||
|
||||
var results: Array = []
|
||||
var result_index: int = -1:
|
||||
set(next_result_index):
|
||||
result_index = next_result_index
|
||||
if results.size() > 0:
|
||||
var r = results[result_index]
|
||||
code_edit.set_caret_line(r[0])
|
||||
code_edit.select(r[0], r[1], r[0], r[1] + r[2])
|
||||
code_edit.center_viewport_to_caret()
|
||||
else:
|
||||
result_index = -1
|
||||
if is_instance_valid(code_edit):
|
||||
code_edit.deselect()
|
||||
|
||||
result_label.text = DialogueConstants.translate(&"n_of_n").format({ index = result_index + 1, total = results.size() })
|
||||
get:
|
||||
return result_index
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
|
||||
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
|
||||
previous_button.tooltip_text = DialogueConstants.translate(&"search.previous")
|
||||
next_button.tooltip_text = DialogueConstants.translate(&"search.next")
|
||||
match_case_button.text = DialogueConstants.translate(&"search.match_case")
|
||||
$Search/ReplaceCheckButton.text = DialogueConstants.translate(&"search.toggle_replace")
|
||||
replace_button.text = DialogueConstants.translate(&"search.replace")
|
||||
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
|
||||
$Replace/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
|
||||
|
||||
self.result_index = -1
|
||||
|
||||
replace_panel.hide()
|
||||
replace_button.disabled = true
|
||||
replace_all_button.disabled = true
|
||||
|
||||
hide()
|
||||
|
||||
|
||||
func focus_line_edit() -> void:
|
||||
input.grab_focus()
|
||||
input.select_all()
|
||||
search()
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
if is_instance_valid(previous_button):
|
||||
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
|
||||
if is_instance_valid(next_button):
|
||||
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
|
||||
|
||||
|
||||
# Find text in the code
|
||||
func search(text: String = "", default_result_index: int = 0) -> void:
|
||||
results.clear()
|
||||
|
||||
if text == "":
|
||||
text = input.text
|
||||
|
||||
var lines = code_edit.text.split("\n")
|
||||
for line_number in range(0, lines.size()):
|
||||
var line = lines[line_number]
|
||||
|
||||
var column = find_in_line(line, text, 0)
|
||||
while column > -1:
|
||||
results.append([line_number, column, text.length()])
|
||||
column = find_in_line(line, text, column + 1)
|
||||
|
||||
if results.size() > 0:
|
||||
replace_button.disabled = false
|
||||
replace_all_button.disabled = false
|
||||
else:
|
||||
replace_button.disabled = true
|
||||
replace_all_button.disabled = true
|
||||
|
||||
self.result_index = clamp(default_result_index, 0, results.size() - 1)
|
||||
|
||||
|
||||
# Find text in a string and match case if requested
|
||||
func find_in_line(line: String, text: String, from_index: int = 0) -> int:
|
||||
if match_case_button.button_pressed:
|
||||
return line.find(text, from_index)
|
||||
else:
|
||||
return line.findn(text, from_index)
|
||||
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_text_edit_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.is_pressed():
|
||||
match event.as_text():
|
||||
"Ctrl+F", "Command+F":
|
||||
open_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
"Ctrl+Shift+R", "Command+Shift+R":
|
||||
replace_check_button.set_pressed(true)
|
||||
open_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_text_edit_text_changed() -> void:
|
||||
results.clear()
|
||||
|
||||
|
||||
func _on_search_and_replace_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_input_text_changed(new_text: String) -> void:
|
||||
search(new_text)
|
||||
|
||||
|
||||
func _on_previous_button_pressed() -> void:
|
||||
self.result_index = wrapi(result_index - 1, 0, results.size())
|
||||
|
||||
|
||||
func _on_next_button_pressed() -> void:
|
||||
self.result_index = wrapi(result_index + 1, 0, results.size())
|
||||
|
||||
|
||||
func _on_search_and_replace_visibility_changed() -> void:
|
||||
if is_instance_valid(input):
|
||||
if visible:
|
||||
input.grab_focus()
|
||||
var selection = code_edit.get_selected_text()
|
||||
if input.text == "" and selection != "":
|
||||
input.text = selection
|
||||
search(selection)
|
||||
else:
|
||||
search()
|
||||
else:
|
||||
input.text = ""
|
||||
|
||||
|
||||
func _on_input_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.is_pressed():
|
||||
match event.as_text():
|
||||
"Enter":
|
||||
if results.size() == 0:
|
||||
search(input.text)
|
||||
self.result_index = wrapi(result_index + 1, 0, results.size())
|
||||
"Escape":
|
||||
emit_signal("close_requested")
|
||||
|
||||
|
||||
func _on_replace_button_pressed() -> void:
|
||||
if result_index == -1: return
|
||||
|
||||
# Replace the selection at result index
|
||||
var r: Array = results[result_index]
|
||||
code_edit.begin_complex_operation()
|
||||
var lines: PackedStringArray = code_edit.text.split("\n")
|
||||
var line: String = lines[r[0]]
|
||||
line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2])
|
||||
lines[r[0]] = line
|
||||
code_edit.text = "\n".join(lines)
|
||||
code_edit.end_complex_operation()
|
||||
code_edit.text_changed.emit()
|
||||
|
||||
search(input.text, result_index)
|
||||
|
||||
|
||||
func _on_replace_all_button_pressed() -> void:
|
||||
if match_case_button.button_pressed:
|
||||
code_edit.text = code_edit.text.replace(input.text, replace_input.text)
|
||||
else:
|
||||
code_edit.text = code_edit.text.replacen(input.text, replace_input.text)
|
||||
search()
|
||||
code_edit.text_changed.emit()
|
||||
|
||||
|
||||
func _on_replace_check_button_toggled(button_pressed: bool) -> void:
|
||||
replace_panel.visible = button_pressed
|
||||
if button_pressed:
|
||||
replace_input.grab_focus()
|
||||
|
||||
|
||||
func _on_input_focus_entered() -> void:
|
||||
if results.size() == 0:
|
||||
search()
|
||||
else:
|
||||
self.result_index = result_index
|
||||
|
||||
|
||||
func _on_match_case_check_box_toggled(button_pressed: bool) -> void:
|
||||
search()
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://cijsmjkq21cdq
|
||||
@@ -0,0 +1,87 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cijsmjkq21cdq" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"]
|
||||
|
||||
[node name="SearchAndReplace" type="VBoxContainer"]
|
||||
visible = false
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 31.0
|
||||
grow_horizontal = 2
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_8oj1f")
|
||||
|
||||
[node name="Search" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Input" type="LineEdit" parent="Search"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Text to search for"
|
||||
metadata/_edit_use_custom_anchors = true
|
||||
|
||||
[node name="MatchCaseCheckBox" type="CheckBox" parent="Search"]
|
||||
layout_mode = 2
|
||||
text = "Match case"
|
||||
|
||||
[node name="VSeparator" type="VSeparator" parent="Search"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PreviousButton" type="Button" parent="Search"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Previous"
|
||||
flat = true
|
||||
|
||||
[node name="ResultLabel" type="Label" parent="Search"]
|
||||
layout_mode = 2
|
||||
text = "0 of 0"
|
||||
|
||||
[node name="NextButton" type="Button" parent="Search"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Next"
|
||||
flat = true
|
||||
|
||||
[node name="VSeparator2" type="VSeparator" parent="Search"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ReplaceCheckButton" type="CheckButton" parent="Search"]
|
||||
layout_mode = 2
|
||||
text = "Replace"
|
||||
|
||||
[node name="Replace" type="HBoxContainer" parent="."]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ReplaceLabel" type="Label" parent="Replace"]
|
||||
layout_mode = 2
|
||||
text = "Replace with:"
|
||||
|
||||
[node name="Input" type="LineEdit" parent="Replace"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ReplaceButton" type="Button" parent="Replace"]
|
||||
layout_mode = 2
|
||||
disabled = true
|
||||
text = "Replace"
|
||||
flat = true
|
||||
|
||||
[node name="ReplaceAllButton" type="Button" parent="Replace"]
|
||||
layout_mode = 2
|
||||
disabled = true
|
||||
text = "Replace all"
|
||||
flat = true
|
||||
|
||||
[connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"]
|
||||
[connection signal="visibility_changed" from="." to="." method="_on_search_and_replace_visibility_changed"]
|
||||
[connection signal="focus_entered" from="Search/Input" to="." method="_on_input_focus_entered"]
|
||||
[connection signal="gui_input" from="Search/Input" to="." method="_on_input_gui_input"]
|
||||
[connection signal="text_changed" from="Search/Input" to="." method="_on_input_text_changed"]
|
||||
[connection signal="toggled" from="Search/MatchCaseCheckBox" to="." method="_on_match_case_check_box_toggled"]
|
||||
[connection signal="pressed" from="Search/PreviousButton" to="." method="_on_previous_button_pressed"]
|
||||
[connection signal="pressed" from="Search/NextButton" to="." method="_on_next_button_pressed"]
|
||||
[connection signal="toggled" from="Search/ReplaceCheckButton" to="." method="_on_replace_check_button_toggled"]
|
||||
[connection signal="focus_entered" from="Replace/Input" to="." method="_on_input_focus_entered"]
|
||||
[connection signal="gui_input" from="Replace/Input" to="." method="_on_input_gui_input"]
|
||||
[connection signal="pressed" from="Replace/ReplaceButton" to="." method="_on_replace_button_pressed"]
|
||||
[connection signal="pressed" from="Replace/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]
|
||||
@@ -0,0 +1,69 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
signal title_selected(title: String)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
|
||||
@onready var filter_edit: LineEdit = $FilterEdit
|
||||
@onready var list: ItemList = $List
|
||||
|
||||
var titles: PackedStringArray:
|
||||
set(next_titles):
|
||||
titles = next_titles
|
||||
apply_filter()
|
||||
get:
|
||||
return titles
|
||||
|
||||
var filter: String:
|
||||
set(next_filter):
|
||||
filter = next_filter
|
||||
apply_filter()
|
||||
get:
|
||||
return filter
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
|
||||
filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter")
|
||||
|
||||
|
||||
func select_title(title: String) -> void:
|
||||
list.deselect_all()
|
||||
for i in range(0, list.get_item_count()):
|
||||
if list.get_item_text(i) == title.strip_edges():
|
||||
list.select(i)
|
||||
|
||||
|
||||
func apply_filter() -> void:
|
||||
list.clear()
|
||||
for title in titles:
|
||||
if filter == "" or filter.to_lower() in title.to_lower():
|
||||
list.add_item(title.strip_edges())
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
if is_instance_valid(filter_edit):
|
||||
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||
if is_instance_valid(list):
|
||||
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_filter_edit_text_changed(new_text: String) -> void:
|
||||
self.filter = new_text
|
||||
|
||||
|
||||
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||
if mouse_button_index == MOUSE_BUTTON_LEFT:
|
||||
var title = list.get_item_text(index)
|
||||
title_selected.emit(title)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0k2wndjj0ifm
|
||||
@@ -0,0 +1,27 @@
|
||||
[gd_scene format=3 uid="uid://ctns6ouwwd68i"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d0k2wndjj0ifm" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"]
|
||||
|
||||
[node name="TitleList" type="VBoxContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_5qqmd")
|
||||
|
||||
[node name="FilterEdit" type="LineEdit" parent="."]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Filter titles"
|
||||
clear_button_enabled = true
|
||||
|
||||
[node name="List" type="ItemList" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
allow_reselect = true
|
||||
|
||||
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
|
||||
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
|
||||
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]
|
||||
@@ -0,0 +1,125 @@
|
||||
@tool
|
||||
extends Button
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
const DialogueSettings = preload("../settings.gd")
|
||||
|
||||
const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases"
|
||||
|
||||
|
||||
@onready var http_request: HTTPRequest = $HTTPRequest
|
||||
@onready var download_dialog: AcceptDialog = $DownloadDialog
|
||||
@onready var download_update_panel = $DownloadDialog/DownloadUpdatePanel
|
||||
@onready var needs_reload_dialog: AcceptDialog = $NeedsReloadDialog
|
||||
@onready var update_failed_dialog: AcceptDialog = $UpdateFailedDialog
|
||||
@onready var timer: Timer = $Timer
|
||||
|
||||
var needs_reload: bool = false
|
||||
|
||||
# A lambda that gets called just before refreshing the plugin. Return false to stop the reload.
|
||||
var on_before_refresh: Callable = func(): return true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
hide()
|
||||
apply_theme()
|
||||
|
||||
# Check for updates on GitHub
|
||||
check_for_update()
|
||||
|
||||
# Check again every few hours
|
||||
timer.start(60 * 60 * 12)
|
||||
|
||||
|
||||
# Convert a version number to an actually comparable number
|
||||
func version_to_number(version: String) -> int:
|
||||
var bits = version.split(".")
|
||||
return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int()
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
var color: Color = get_theme_color("success_color", "Editor")
|
||||
|
||||
if needs_reload:
|
||||
color = get_theme_color("error_color", "Editor")
|
||||
icon = get_theme_icon("Reload", "EditorIcons")
|
||||
add_theme_color_override("icon_normal_color", color)
|
||||
add_theme_color_override("icon_focus_color", color)
|
||||
add_theme_color_override("icon_hover_color", color)
|
||||
|
||||
add_theme_color_override("font_color", color)
|
||||
add_theme_color_override("font_focus_color", color)
|
||||
add_theme_color_override("font_hover_color", color)
|
||||
|
||||
|
||||
func check_for_update() -> void:
|
||||
if DialogueSettings.get_user_value("check_for_updates", true):
|
||||
http_request.request(REMOTE_RELEASES_URL)
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
if result != HTTPRequest.RESULT_SUCCESS: return
|
||||
|
||||
var current_version: String = DMPlugin.get_version()
|
||||
|
||||
# Work out the next version from the releases information on GitHub
|
||||
var response = JSON.parse_string(body.get_string_from_utf8())
|
||||
if typeof(response) != TYPE_ARRAY: return
|
||||
|
||||
# GitHub releases are in order of creation, not order of version
|
||||
var versions = (response as Array).filter(func(release):
|
||||
var version: String = release.tag_name.substr(1)
|
||||
var major_version: int = version.split(".")[0].to_int()
|
||||
var current_major_version: int = current_version.split(".")[0].to_int()
|
||||
return major_version == current_major_version and version_to_number(version) > version_to_number(current_version)
|
||||
)
|
||||
if versions.size() > 0:
|
||||
download_update_panel.next_version_release = versions[0]
|
||||
text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) })
|
||||
show()
|
||||
|
||||
|
||||
func _on_update_button_pressed() -> void:
|
||||
if needs_reload:
|
||||
var will_refresh = on_before_refresh.call()
|
||||
if will_refresh:
|
||||
EditorInterface.restart_editor(true)
|
||||
else:
|
||||
var scale: float = EditorInterface.get_editor_scale()
|
||||
download_dialog.min_size = Vector2(300, 250) * scale
|
||||
download_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_download_dialog_close_requested() -> void:
|
||||
download_dialog.hide()
|
||||
|
||||
|
||||
func _on_download_update_panel_updated(updated_to_version: String) -> void:
|
||||
download_dialog.hide()
|
||||
|
||||
needs_reload_dialog.dialog_text = DialogueConstants.translate(&"update.needs_reload")
|
||||
needs_reload_dialog.ok_button_text = DialogueConstants.translate(&"update.reload_ok_button")
|
||||
needs_reload_dialog.cancel_button_text = DialogueConstants.translate(&"update.reload_cancel_button")
|
||||
needs_reload_dialog.popup_centered()
|
||||
|
||||
needs_reload = true
|
||||
text = DialogueConstants.translate(&"update.reload_project")
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_download_update_panel_failed() -> void:
|
||||
download_dialog.hide()
|
||||
update_failed_dialog.dialog_text = DialogueConstants.translate(&"update.failed")
|
||||
update_failed_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_needs_reload_dialog_confirmed() -> void:
|
||||
EditorInterface.restart_editor(true)
|
||||
|
||||
|
||||
func _on_timer_timeout() -> void:
|
||||
if not needs_reload:
|
||||
check_for_update()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cr1tt12dh5ecr
|
||||
@@ -0,0 +1,42 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cr1tt12dh5ecr" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"]
|
||||
[ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"]
|
||||
|
||||
[node name="UpdateButton" type="Button"]
|
||||
visible = false
|
||||
offset_right = 8.0
|
||||
offset_bottom = 8.0
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
|
||||
theme_override_colors/font_focus_color = Color(0, 0, 0, 1)
|
||||
text = "v2.9.0 available"
|
||||
flat = true
|
||||
script = ExtResource("1_d2tpb")
|
||||
|
||||
[node name="HTTPRequest" type="HTTPRequest" parent="."]
|
||||
|
||||
[node name="DownloadDialog" type="AcceptDialog" parent="."]
|
||||
title = "Download update"
|
||||
size = Vector2i(400, 300)
|
||||
unresizable = true
|
||||
min_size = Vector2i(300, 250)
|
||||
ok_button_text = "Close"
|
||||
|
||||
[node name="DownloadUpdatePanel" parent="DownloadDialog" instance=ExtResource("2_iwm7r")]
|
||||
|
||||
[node name="UpdateFailedDialog" type="AcceptDialog" parent="."]
|
||||
dialog_text = "You have been updated to version 2.4.3"
|
||||
|
||||
[node name="NeedsReloadDialog" type="ConfirmationDialog" parent="."]
|
||||
|
||||
[node name="Timer" type="Timer" parent="."]
|
||||
wait_time = 14400.0
|
||||
|
||||
[connection signal="pressed" from="." to="." method="_on_update_button_pressed"]
|
||||
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
|
||||
[connection signal="close_requested" from="DownloadDialog" to="." method="_on_download_dialog_close_requested"]
|
||||
[connection signal="failed" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_failed"]
|
||||
[connection signal="updated" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_updated"]
|
||||
[connection signal="confirmed" from="NeedsReloadDialog" to="." method="_on_needs_reload_dialog_confirmed"]
|
||||
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
|
||||
@@ -0,0 +1,240 @@
|
||||
class_name DMConstants extends RefCounted
|
||||
|
||||
|
||||
const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json"
|
||||
const CACHE_PATH = "user://dialogue_manager_cache.json"
|
||||
|
||||
|
||||
enum MutationBehaviour {
|
||||
Wait,
|
||||
DoNotWait,
|
||||
Skip
|
||||
}
|
||||
|
||||
enum TranslationSource {
|
||||
None,
|
||||
Guess,
|
||||
CSV,
|
||||
PO
|
||||
}
|
||||
|
||||
# Token types
|
||||
|
||||
const TOKEN_FUNCTION = &"function"
|
||||
const TOKEN_DICTIONARY_REFERENCE = &"dictionary_reference"
|
||||
const TOKEN_DICTIONARY_NESTED_REFERENCE = &"dictionary_nested_reference"
|
||||
const TOKEN_GROUP = &"group"
|
||||
const TOKEN_ARRAY = &"array"
|
||||
const TOKEN_DICTIONARY = &"dictionary"
|
||||
const TOKEN_PARENS_OPEN = &"parens_open"
|
||||
const TOKEN_PARENS_CLOSE = &"parens_close"
|
||||
const TOKEN_BRACKET_OPEN = &"bracket_open"
|
||||
const TOKEN_BRACKET_CLOSE = &"bracket_close"
|
||||
const TOKEN_BRACE_OPEN = &"brace_open"
|
||||
const TOKEN_BRACE_CLOSE = &"brace_close"
|
||||
const TOKEN_COLON = &"colon"
|
||||
const TOKEN_COMPARISON = &"comparison"
|
||||
const TOKEN_ASSIGNMENT = &"assignment"
|
||||
const TOKEN_OPERATOR = &"operator"
|
||||
const TOKEN_COMMA = &"comma"
|
||||
const TOKEN_NULL_COALESCE = &"null_coalesce"
|
||||
const TOKEN_DOT = &"dot"
|
||||
const TOKEN_CONDITION = &"condition"
|
||||
const TOKEN_BOOL = &"bool"
|
||||
const TOKEN_NOT = &"not"
|
||||
const TOKEN_AND_OR = &"and_or"
|
||||
const TOKEN_STRING = &"string"
|
||||
const TOKEN_NUMBER = &"number"
|
||||
const TOKEN_VARIABLE = &"variable"
|
||||
const TOKEN_COMMENT = &"comment"
|
||||
|
||||
const TOKEN_VALUE = &"value"
|
||||
const TOKEN_ERROR = &"error"
|
||||
|
||||
# Line types
|
||||
|
||||
const TYPE_UNKNOWN = &""
|
||||
const TYPE_IMPORT = &"import"
|
||||
const TYPE_USING = &"using"
|
||||
const TYPE_COMMENT = &"comment"
|
||||
const TYPE_RESPONSE = &"response"
|
||||
const TYPE_TITLE = &"title"
|
||||
const TYPE_CONDITION = &"condition"
|
||||
const TYPE_WHILE = &"while"
|
||||
const TYPE_MATCH = &"match"
|
||||
const TYPE_WHEN = &"when"
|
||||
const TYPE_MUTATION = &"mutation"
|
||||
const TYPE_GOTO = &"goto"
|
||||
const TYPE_DIALOGUE = &"dialogue"
|
||||
const TYPE_RANDOM = &"random"
|
||||
const TYPE_ERROR = &"error"
|
||||
|
||||
# Line IDs
|
||||
|
||||
const ID_NULL = &""
|
||||
const ID_ERROR = &"error"
|
||||
const ID_ERROR_INVALID_TITLE = &"invalid title"
|
||||
const ID_ERROR_TITLE_HAS_NO_BODY = &"title has no body"
|
||||
const ID_END = &"end"
|
||||
const ID_END_CONVERSATION = &"end!"
|
||||
|
||||
# Errors
|
||||
|
||||
const ERR_ERRORS_IN_IMPORTED_FILE = 100
|
||||
const ERR_FILE_ALREADY_IMPORTED = 101
|
||||
const ERR_DUPLICATE_IMPORT_NAME = 102
|
||||
const ERR_EMPTY_TITLE = 103
|
||||
const ERR_DUPLICATE_TITLE = 104
|
||||
const ERR_TITLE_INVALID_CHARACTERS = 106
|
||||
const ERR_UNKNOWN_TITLE = 107
|
||||
const ERR_INVALID_TITLE_REFERENCE = 108
|
||||
const ERR_TITLE_REFERENCE_HAS_NO_CONTENT = 109
|
||||
const ERR_INVALID_EXPRESSION = 110
|
||||
const ERR_UNEXPECTED_CONDITION = 111
|
||||
const ERR_DUPLICATE_ID = 112
|
||||
const ERR_MISSING_ID = 113
|
||||
const ERR_INVALID_INDENTATION = 114
|
||||
const ERR_INVALID_CONDITION_INDENTATION = 115
|
||||
const ERR_INCOMPLETE_EXPRESSION = 116
|
||||
const ERR_INVALID_EXPRESSION_FOR_VALUE = 117
|
||||
const ERR_UNKNOWN_LINE_SYNTAX = 118
|
||||
const ERR_TITLE_BEGINS_WITH_NUMBER = 119
|
||||
const ERR_UNEXPECTED_END_OF_EXPRESSION = 120
|
||||
const ERR_UNEXPECTED_FUNCTION = 121
|
||||
const ERR_UNEXPECTED_BRACKET = 122
|
||||
const ERR_UNEXPECTED_CLOSING_BRACKET = 123
|
||||
const ERR_MISSING_CLOSING_BRACKET = 124
|
||||
const ERR_UNEXPECTED_OPERATOR = 125
|
||||
const ERR_UNEXPECTED_COMMA = 126
|
||||
const ERR_UNEXPECTED_COLON = 127
|
||||
const ERR_UNEXPECTED_DOT = 128
|
||||
const ERR_UNEXPECTED_BOOLEAN = 129
|
||||
const ERR_UNEXPECTED_STRING = 130
|
||||
const ERR_UNEXPECTED_NUMBER = 131
|
||||
const ERR_UNEXPECTED_VARIABLE = 132
|
||||
const ERR_INVALID_INDEX = 133
|
||||
const ERR_UNEXPECTED_ASSIGNMENT = 134
|
||||
const ERR_UNKNOWN_USING = 135
|
||||
const ERR_EXPECTED_WHEN_OR_ELSE = 136
|
||||
const ERR_ONLY_ONE_ELSE_ALLOWED = 137
|
||||
const ERR_WHEN_MUST_BELONG_TO_MATCH = 138
|
||||
const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139
|
||||
const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140
|
||||
const ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE = 141
|
||||
const ERR_NESTED_DIALOGUE_INVALID_JUMP = 142
|
||||
const ERR_MISSING_RESOURCE_FOR_AUTOSTART = 143
|
||||
const ERR_LONELY_STATIC_ID = 144
|
||||
|
||||
|
||||
static var _current_locale: String = ""
|
||||
static var _current_translation: Translation
|
||||
static var _en_translation: Translation
|
||||
|
||||
|
||||
## Get the error message
|
||||
static func get_error_message(error: int) -> String:
|
||||
match error:
|
||||
ERR_ERRORS_IN_IMPORTED_FILE:
|
||||
return translate(&"errors.import_errors")
|
||||
ERR_FILE_ALREADY_IMPORTED:
|
||||
return translate(&"errors.already_imported")
|
||||
ERR_DUPLICATE_IMPORT_NAME:
|
||||
return translate(&"errors.duplicate_import")
|
||||
ERR_EMPTY_TITLE:
|
||||
return translate(&"errors.empty_title")
|
||||
ERR_DUPLICATE_TITLE:
|
||||
return translate(&"errors.duplicate_title")
|
||||
ERR_TITLE_INVALID_CHARACTERS:
|
||||
return translate(&"errors.invalid_title_string")
|
||||
ERR_TITLE_BEGINS_WITH_NUMBER:
|
||||
return translate(&"errors.invalid_title_number")
|
||||
ERR_UNKNOWN_TITLE:
|
||||
return translate(&"errors.unknown_title")
|
||||
ERR_INVALID_TITLE_REFERENCE:
|
||||
return translate(&"errors.jump_to_invalid_title")
|
||||
ERR_TITLE_REFERENCE_HAS_NO_CONTENT:
|
||||
return translate(&"errors.title_has_no_content")
|
||||
ERR_INVALID_EXPRESSION:
|
||||
return translate(&"errors.invalid_expression")
|
||||
ERR_UNEXPECTED_CONDITION:
|
||||
return translate(&"errors.unexpected_condition")
|
||||
ERR_DUPLICATE_ID:
|
||||
return translate(&"errors.duplicate_id")
|
||||
ERR_MISSING_ID:
|
||||
return translate(&"errors.missing_id")
|
||||
ERR_INVALID_INDENTATION:
|
||||
return translate(&"errors.invalid_indentation")
|
||||
ERR_INVALID_CONDITION_INDENTATION:
|
||||
return translate(&"errors.condition_has_no_content")
|
||||
ERR_INCOMPLETE_EXPRESSION:
|
||||
return translate(&"errors.incomplete_expression")
|
||||
ERR_INVALID_EXPRESSION_FOR_VALUE:
|
||||
return translate(&"errors.invalid_expression_for_value")
|
||||
ERR_FILE_NOT_FOUND:
|
||||
return translate(&"errors.file_not_found")
|
||||
ERR_UNEXPECTED_END_OF_EXPRESSION:
|
||||
return translate(&"errors.unexpected_end_of_expression")
|
||||
ERR_UNEXPECTED_FUNCTION:
|
||||
return translate(&"errors.unexpected_function")
|
||||
ERR_UNEXPECTED_BRACKET:
|
||||
return translate(&"errors.unexpected_bracket")
|
||||
ERR_UNEXPECTED_CLOSING_BRACKET:
|
||||
return translate(&"errors.unexpected_closing_bracket")
|
||||
ERR_MISSING_CLOSING_BRACKET:
|
||||
return translate(&"errors.missing_closing_bracket")
|
||||
ERR_UNEXPECTED_OPERATOR:
|
||||
return translate(&"errors.unexpected_operator")
|
||||
ERR_UNEXPECTED_COMMA:
|
||||
return translate(&"errors.unexpected_comma")
|
||||
ERR_UNEXPECTED_COLON:
|
||||
return translate(&"errors.unexpected_colon")
|
||||
ERR_UNEXPECTED_DOT:
|
||||
return translate(&"errors.unexpected_dot")
|
||||
ERR_UNEXPECTED_BOOLEAN:
|
||||
return translate(&"errors.unexpected_boolean")
|
||||
ERR_UNEXPECTED_STRING:
|
||||
return translate(&"errors.unexpected_string")
|
||||
ERR_UNEXPECTED_NUMBER:
|
||||
return translate(&"errors.unexpected_number")
|
||||
ERR_UNEXPECTED_VARIABLE:
|
||||
return translate(&"errors.unexpected_variable")
|
||||
ERR_INVALID_INDEX:
|
||||
return translate(&"errors.invalid_index")
|
||||
ERR_UNEXPECTED_ASSIGNMENT:
|
||||
return translate(&"errors.unexpected_assignment")
|
||||
ERR_UNKNOWN_USING:
|
||||
return translate(&"errors.unknown_using")
|
||||
ERR_EXPECTED_WHEN_OR_ELSE:
|
||||
return translate(&"errors.expected_when_or_else")
|
||||
ERR_ONLY_ONE_ELSE_ALLOWED:
|
||||
return translate(&"errors.only_one_else_allowed")
|
||||
ERR_WHEN_MUST_BELONG_TO_MATCH:
|
||||
return translate(&"errors.when_must_belong_to_match")
|
||||
ERR_CONCURRENT_LINE_WITHOUT_ORIGIN:
|
||||
return translate(&"errors.concurrent_line_without_origin")
|
||||
ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES:
|
||||
return translate(&"errors.goto_not_allowed_on_concurrect_lines")
|
||||
ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE:
|
||||
return translate(&"errors.unexpected_syntax_on_nested_dialogue_line")
|
||||
ERR_NESTED_DIALOGUE_INVALID_JUMP:
|
||||
return translate(&"errors.err_nested_dialogue_invalid_jump")
|
||||
ERR_MISSING_RESOURCE_FOR_AUTOSTART:
|
||||
return translate(&"errors.missing_resource_for_autostart")
|
||||
ERR_LONELY_STATIC_ID:
|
||||
return translate(&"Static ID can't be on a line with no other content.")
|
||||
|
||||
return translate(&"errors.unknown")
|
||||
|
||||
|
||||
static func translate(string: String) -> String:
|
||||
var locale: String = TranslationServer.get_tool_locale()
|
||||
if _current_translation == null or _current_locale != locale:
|
||||
var base_path: String = new().get_script().resource_path.get_base_dir()
|
||||
var translation_path: String = "%s/l10n/%s.po" % [base_path, locale]
|
||||
var fallback_translation_path: String = "%s/l10n/%s.po" % [base_path, locale.substr(0, 2)]
|
||||
var en_translation_path: String = "%s/l10n/en.po" % base_path
|
||||
_current_translation = load(translation_path if FileAccess.file_exists(translation_path) else (fallback_translation_path if FileAccess.file_exists(fallback_translation_path) else en_translation_path))
|
||||
_en_translation = load(en_translation_path)
|
||||
_current_locale = locale
|
||||
var message: String = _current_translation.get_message(string)
|
||||
return message if not message.is_empty() else _en_translation.get_message(string)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1oarbmjtyesf
|
||||
@@ -0,0 +1,227 @@
|
||||
@icon("./assets/icon.svg")
|
||||
|
||||
@tool
|
||||
|
||||
## A RichTextLabel specifically for use with [b]Dialogue Manager[/b] dialogue.
|
||||
class_name DialogueLabel extends RichTextLabel
|
||||
|
||||
|
||||
## Emitted for each letter typed out.
|
||||
signal spoke(letter: String, letter_index: int, speed: float)
|
||||
|
||||
## Emitted when the player skips the typing of dialogue.
|
||||
signal skipped_typing()
|
||||
|
||||
## Emitted when typing starts
|
||||
signal started_typing()
|
||||
|
||||
## Emitted when typing finishes.
|
||||
signal finished_typing()
|
||||
|
||||
## [Deprecated] No longer emitted.
|
||||
signal paused_typing(duration: float)
|
||||
|
||||
|
||||
## The action to press to skip typing.
|
||||
@export var skip_action: StringName = &"ui_cancel"
|
||||
|
||||
## The speed with which the text types out.
|
||||
@export var seconds_per_step: float = 0.02
|
||||
|
||||
## Automatically have a brief pause when these characters are encountered.
|
||||
@export var pause_at_characters: String = ".?!"
|
||||
|
||||
## Don't auto pause if the character after the pause is one of these.
|
||||
@export var skip_pause_at_character_if_followed_by: String = ")\""
|
||||
|
||||
## Don't auto pause after these abbreviations (only if "." is in `pause_at_characters`).[br]
|
||||
## Abbreviations are limitted to 5 characters in length [br]
|
||||
## Does not support multi-period abbreviations (ex. "p.m.")
|
||||
@export var skip_pause_at_abbreviations: PackedStringArray = ["Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex"]
|
||||
|
||||
## The amount of time to pause when exposing a character present in `pause_at_characters`.
|
||||
@export var seconds_per_pause_step: float = 0.3
|
||||
|
||||
var _already_mutated_indices: PackedInt32Array = []
|
||||
|
||||
|
||||
## The current line of dialogue.
|
||||
var dialogue_line:
|
||||
set(value):
|
||||
if value != dialogue_line:
|
||||
dialogue_line = value
|
||||
_update_text()
|
||||
get:
|
||||
return dialogue_line
|
||||
|
||||
## Whether the label is currently typing itself out.
|
||||
var is_typing: bool = false:
|
||||
set(value):
|
||||
var is_finished: bool = _is_typing != value and value == false and visible_characters == get_total_character_count()
|
||||
_is_typing = value
|
||||
if is_finished:
|
||||
finished_typing.emit()
|
||||
get:
|
||||
return _is_typing and not _is_awaiting_mutation
|
||||
var _is_typing: bool = false
|
||||
|
||||
var _last_wait_index: int = -1
|
||||
var _last_mutation_index: int = -1
|
||||
var _waiting_seconds: float = 0
|
||||
var _is_awaiting_mutation: bool = false
|
||||
var _is_skipping_mutations: bool = false
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _is_typing:
|
||||
# Type out text
|
||||
if visible_ratio < 1:
|
||||
# See if we are waiting
|
||||
if _waiting_seconds > 0:
|
||||
_waiting_seconds = _waiting_seconds - delta
|
||||
# If we are no longer waiting then keep typing
|
||||
if _waiting_seconds <= 0:
|
||||
_type_next(delta, _waiting_seconds)
|
||||
else:
|
||||
# Make sure any mutations at the end of the line get run
|
||||
_mutate_inline_mutations(get_total_character_count())
|
||||
is_typing = false
|
||||
|
||||
|
||||
## Sets the label's text from the current dialogue line. Override if you want
|
||||
## to do something more interesting in your subclass.
|
||||
func _update_text() -> void:
|
||||
text = dialogue_line.text
|
||||
|
||||
|
||||
## Start typing out the text
|
||||
func type_out() -> void:
|
||||
_update_text()
|
||||
visible_characters = 0
|
||||
visible_ratio = 0
|
||||
_waiting_seconds = 0
|
||||
_last_wait_index = -1
|
||||
_last_mutation_index = -1
|
||||
_already_mutated_indices.clear()
|
||||
|
||||
is_typing = true
|
||||
started_typing.emit()
|
||||
|
||||
# Allow typing listeners a chance to connect
|
||||
await get_tree().process_frame
|
||||
|
||||
if get_total_character_count() == 0:
|
||||
is_typing = false
|
||||
elif seconds_per_step == 0:
|
||||
_mutate_remaining_mutations()
|
||||
visible_characters = get_total_character_count()
|
||||
is_typing = false
|
||||
|
||||
|
||||
## Stop typing out the text and jump right to the end
|
||||
func skip_typing() -> void:
|
||||
_mutate_remaining_mutations()
|
||||
visible_characters = get_total_character_count()
|
||||
is_typing = false
|
||||
skipped_typing.emit()
|
||||
|
||||
|
||||
# Type out the next character(s)
|
||||
func _type_next(delta: float, seconds_needed: float) -> void:
|
||||
if _is_awaiting_mutation: return
|
||||
|
||||
if visible_characters == get_total_character_count():
|
||||
return
|
||||
|
||||
if _last_mutation_index != visible_characters:
|
||||
_last_mutation_index = visible_characters
|
||||
_mutate_inline_mutations(visible_characters)
|
||||
if _is_awaiting_mutation: return
|
||||
|
||||
# Pause on characters like "."
|
||||
var waiting_seconds: float = seconds_per_pause_step if _should_auto_pause() else 0
|
||||
if _last_wait_index != visible_characters and waiting_seconds > 0:
|
||||
_last_wait_index = visible_characters
|
||||
_waiting_seconds += waiting_seconds
|
||||
else:
|
||||
visible_characters += 1
|
||||
if visible_characters <= get_total_character_count():
|
||||
spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, _get_speed(visible_characters))
|
||||
# See if there's time to type out some more in this frame
|
||||
seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters))
|
||||
if seconds_needed > delta:
|
||||
_waiting_seconds += seconds_needed
|
||||
else:
|
||||
_type_next(delta, seconds_needed)
|
||||
|
||||
|
||||
# Get the speed for the current typing position
|
||||
func _get_speed(at_index: int) -> float:
|
||||
var speed: float = 1
|
||||
for index in dialogue_line.speeds:
|
||||
if index > at_index:
|
||||
return speed
|
||||
speed = dialogue_line.speeds[index]
|
||||
return speed
|
||||
|
||||
|
||||
# Run any inline mutations that haven't been run yet
|
||||
func _mutate_remaining_mutations() -> void:
|
||||
_is_skipping_mutations = true
|
||||
for i in range(visible_characters, get_total_character_count() + 1):
|
||||
_mutate_inline_mutations(i)
|
||||
_is_skipping_mutations = false
|
||||
|
||||
|
||||
# Run any mutations at the current typing position
|
||||
func _mutate_inline_mutations(index: int) -> void:
|
||||
for inline_mutation in dialogue_line.inline_mutations:
|
||||
# inline mutations are an array of arrays in the form of [character index, resolvable function]
|
||||
if inline_mutation[0] > index:
|
||||
return
|
||||
if inline_mutation[0] == index and not _already_mutated_indices.has(index):
|
||||
if _is_skipping_mutations:
|
||||
Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
|
||||
else:
|
||||
_is_awaiting_mutation = true
|
||||
await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
|
||||
_is_awaiting_mutation = false
|
||||
|
||||
_already_mutated_indices.append(index)
|
||||
|
||||
|
||||
# Determine if the current autopause character at the cursor should qualify to pause typing.
|
||||
func _should_auto_pause() -> bool:
|
||||
if visible_characters == 0: return false
|
||||
|
||||
var parsed_text: String = get_parsed_text()
|
||||
|
||||
# Avoid outofbounds when the label auto-translates and the text changes to one shorter while typing out
|
||||
# Note: visible characters can be larger than parsed_text after a translation event
|
||||
if visible_characters >= parsed_text.length(): return false
|
||||
|
||||
# Ignore pause characters if they are next to a non-pause character
|
||||
if parsed_text[visible_characters] in skip_pause_at_character_if_followed_by.split():
|
||||
return false
|
||||
|
||||
# Ignore "." if it's between two numbers
|
||||
if visible_characters > 3 and parsed_text[visible_characters - 1] == ".":
|
||||
var possible_number: String = parsed_text.substr(visible_characters - 2, 3)
|
||||
if str(float(possible_number)).pad_decimals(1) == possible_number:
|
||||
return false
|
||||
|
||||
# Ignore "." if it's used in an abbreviation
|
||||
# Note: does NOT support multi-period abbreviations (ex. p.m.)
|
||||
if "." in pause_at_characters and parsed_text[visible_characters - 1] == ".":
|
||||
for abbreviation in skip_pause_at_abbreviations:
|
||||
if visible_characters >= abbreviation.length():
|
||||
var previous_characters: String = parsed_text.substr(visible_characters - abbreviation.length() - 1, abbreviation.length())
|
||||
if previous_characters == abbreviation:
|
||||
return false
|
||||
|
||||
# Ignore two non-"." characters next to each other
|
||||
var other_pause_characters: PackedStringArray = pause_at_characters.replace(".", "").split()
|
||||
if visible_characters > 1 and parsed_text[visible_characters - 1] in other_pause_characters and parsed_text[visible_characters] in other_pause_characters:
|
||||
return false
|
||||
|
||||
return parsed_text[visible_characters - 1] in pause_at_characters.split()
|
||||
@@ -0,0 +1 @@
|
||||
uid://g32um0mltv5d
|
||||
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://g32um0mltv5d" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"]
|
||||
|
||||
[node name="DialogueLabel" type="RichTextLabel"]
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
shortcut_keys_enabled = false
|
||||
meta_underlined = false
|
||||
hint_underlined = false
|
||||
deselect_on_focus_loss_enabled = false
|
||||
visible_characters_behavior = 1
|
||||
script = ExtResource("1_cital")
|
||||
skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex")
|
||||
@@ -0,0 +1,108 @@
|
||||
## A line of dialogue returned from [code]DialogueManager[/code].
|
||||
class_name DialogueLine extends RefCounted
|
||||
|
||||
|
||||
## The ID of this line
|
||||
var id: String
|
||||
|
||||
## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code]
|
||||
var type: String = DMConstants.TYPE_DIALOGUE
|
||||
|
||||
## The next line ID after this line.
|
||||
var next_id: String = ""
|
||||
|
||||
## The character name that is saying this line.
|
||||
var character: String = ""
|
||||
|
||||
## A dictionary of variable replacements fo the character name. Generally for internal use only.
|
||||
var character_replacements: Array[Dictionary] = []
|
||||
|
||||
## The dialogue being spoken.
|
||||
var text: String = ""
|
||||
|
||||
## A dictionary of replacements for the text. Generally for internal use only.
|
||||
var text_replacements: Array[Dictionary] = []
|
||||
|
||||
## The key to use for translating this line.
|
||||
var translation_key: String = ""
|
||||
|
||||
## A map for speed changes when typing out the dialogue text.
|
||||
var speeds: Dictionary = {}
|
||||
|
||||
## A map of any mutations to run while typing out the dialogue text.
|
||||
var inline_mutations: Array[Array] = []
|
||||
|
||||
## A list of responses attached to this line of dialogue.
|
||||
var responses: Array = []
|
||||
|
||||
## A list of lines that are spoken simultaneously with this one.
|
||||
var concurrent_lines: Array[DialogueLine] = []
|
||||
|
||||
## A list of any extra game states to check when resolving variables and mutations.
|
||||
var extra_game_states: Array = []
|
||||
|
||||
## How long to show this line before advancing to the next. Either a float (of seconds), [code]"auto"[/code], or [code]null[/code].
|
||||
var time: String = ""
|
||||
|
||||
## Any #tags that were included in the line
|
||||
var tags: PackedStringArray = []
|
||||
|
||||
## The mutation details if this is a mutation line (where [code]type == TYPE_MUTATION[/code]).
|
||||
var mutation: Dictionary = {}
|
||||
|
||||
## The conditions to check before including this line in the flow of dialogue. If failed the line will be skipped over.
|
||||
var conditions: Dictionary = {}
|
||||
|
||||
|
||||
func _init(data: Dictionary = {}) -> void:
|
||||
if data.size() > 0:
|
||||
id = data.id
|
||||
next_id = data.next_id
|
||||
type = data.type
|
||||
extra_game_states = data.get("extra_game_states", [])
|
||||
|
||||
match type:
|
||||
DMConstants.TYPE_DIALOGUE:
|
||||
character = data.character
|
||||
character_replacements = data.get("character_replacements", [] as Array[Dictionary])
|
||||
text = data.text
|
||||
text_replacements = data.get("text_replacements", [] as Array[Dictionary])
|
||||
translation_key = data.get("translation_key", data.text)
|
||||
speeds = data.get("speeds", {})
|
||||
inline_mutations = data.get("inline_mutations", [] as Array[Array])
|
||||
time = data.get("time", "")
|
||||
tags = data.get("tags", [])
|
||||
concurrent_lines = data.get("concurrent_lines", [] as Array[DialogueLine])
|
||||
|
||||
DMConstants.TYPE_MUTATION:
|
||||
mutation = data.mutation
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
match type:
|
||||
DMConstants.TYPE_DIALOGUE:
|
||||
return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text]
|
||||
DMConstants.TYPE_MUTATION:
|
||||
return "<DialogueLine mutation>"
|
||||
return ""
|
||||
|
||||
|
||||
## Check if a dialogue line has a given tag.
|
||||
func has_tag(tag_name: String) -> bool:
|
||||
if tags.has(tag_name):
|
||||
return true
|
||||
else:
|
||||
var wrapped: String = "%s=" % tag_name
|
||||
for t in tags:
|
||||
if t.begins_with(wrapped):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Get the value of a tag if the tag is in the form of [code]tag=value[/code]
|
||||
func get_tag_value(tag_name: String) -> String:
|
||||
var wrapped: String = "%s=" % tag_name
|
||||
for t in tags:
|
||||
if t.begins_with(wrapped):
|
||||
return t.replace(wrapped, "").strip_edges()
|
||||
return ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://rhuq0eyf8ar2
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://c3rodes2l3gxb
|
||||
@@ -0,0 +1,11 @@
|
||||
class_name DMDialogueProcessor extends RefCounted
|
||||
|
||||
|
||||
## Override to modify the incoming raw string.
|
||||
func _preprocess_line(raw_line: String) -> String:
|
||||
return raw_line
|
||||
|
||||
|
||||
## Override to modify the outgoing dialogue line.
|
||||
func _process_line(line: DMCompiledLine) -> void:
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://m3b28rmso14t
|
||||
@@ -0,0 +1,45 @@
|
||||
@tool
|
||||
@icon("./assets/icon.svg")
|
||||
|
||||
## A collection of dialogue lines for use with [code]DialogueManager[/code].
|
||||
class_name DialogueResource extends Resource
|
||||
|
||||
|
||||
const DialogueLine = preload("./dialogue_line.gd")
|
||||
|
||||
## A list of state shortcuts
|
||||
@export var using_states: PackedStringArray = []
|
||||
|
||||
## A map of titles and the lines they point to.
|
||||
@export var titles: Dictionary = {}
|
||||
|
||||
## A list of character names.
|
||||
@export var character_names: PackedStringArray = []
|
||||
|
||||
## The first title in the file.
|
||||
@export var first_title: String = ""
|
||||
|
||||
## A map of the encoded lines of dialogue.
|
||||
@export var lines: Dictionary = {}
|
||||
|
||||
## raw version of the text
|
||||
@export var raw_text: String
|
||||
|
||||
|
||||
## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can
|
||||
## be a title string or a stringified line number). Runs any mutations along the way and then returns
|
||||
## the first dialogue line encountered.
|
||||
func get_next_dialogue_line(title: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine:
|
||||
return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour)
|
||||
|
||||
|
||||
## Get the list of any titles found in the file.
|
||||
func get_titles() -> PackedStringArray:
|
||||
return titles.keys()
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
if resource_path:
|
||||
return "<DialogueResource path=\"%s\">" % [resource_path]
|
||||
else:
|
||||
return "<DialogueResource>"
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbs4435dsf3ry
|
||||
@@ -0,0 +1,63 @@
|
||||
## A response to a line of dialogue, usualy attached to a [code]DialogueLine[/code].
|
||||
class_name DialogueResponse extends RefCounted
|
||||
|
||||
|
||||
## The ID of this response
|
||||
var id: String
|
||||
|
||||
## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code].
|
||||
var type: String = DMConstants.TYPE_RESPONSE
|
||||
|
||||
## The next line ID to use if this response is selected by the player.
|
||||
var next_id: String = ""
|
||||
|
||||
## [code]true[/code] if the condition of this line was met.
|
||||
var is_allowed: bool = true
|
||||
|
||||
## The original condition text.
|
||||
var condition_as_text: String = ""
|
||||
|
||||
## A character (depending on the "characters in responses" behaviour setting).
|
||||
var character: String = ""
|
||||
|
||||
## A dictionary of varialbe replaces for the character name. Generally for internal use only.
|
||||
var character_replacements: Array[Dictionary] = []
|
||||
|
||||
## The prompt for this response.
|
||||
var text: String = ""
|
||||
|
||||
## A dictionary of variable replaces for the text. Generally for internal use only.
|
||||
var text_replacements: Array[Dictionary] = []
|
||||
|
||||
## Any #tags
|
||||
var tags: PackedStringArray = []
|
||||
|
||||
## The key to use for translating the text.
|
||||
var translation_key: String = ""
|
||||
|
||||
|
||||
func _init(data: Dictionary = {}) -> void:
|
||||
if data.size() > 0:
|
||||
id = data.id
|
||||
type = data.type
|
||||
next_id = data.next_id
|
||||
is_allowed = data.is_allowed
|
||||
character = data.character
|
||||
character_replacements = data.character_replacements
|
||||
text = data.text
|
||||
text_replacements = data.text_replacements
|
||||
tags = data.tags
|
||||
translation_key = data.translation_key
|
||||
condition_as_text = data.condition_as_text
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "<DialogueResponse text=\"%s\">" % text
|
||||
|
||||
|
||||
func get_tag_value(tag_name: String) -> String:
|
||||
var wrapped := "%s=" % tag_name
|
||||
for t in tags:
|
||||
if t.begins_with(wrapped):
|
||||
return t.replace(wrapped, "").strip_edges()
|
||||
return ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://cm0xpfeywpqid
|
||||
@@ -0,0 +1,178 @@
|
||||
@icon("./assets/responses_menu.svg")
|
||||
|
||||
## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b].
|
||||
class_name DialogueResponsesMenu extends Container
|
||||
|
||||
|
||||
## Emitted when a response is focused.
|
||||
signal response_focused(response: Control)
|
||||
|
||||
## Emitted when a response is selected.
|
||||
signal response_selected(response: Control)
|
||||
|
||||
|
||||
## Optionally specify a control to duplicate for each response
|
||||
@export var response_template: Control
|
||||
|
||||
## The action for accepting a response (is possibly overridden by parent dialogue balloon).
|
||||
@export var next_action: StringName = &""
|
||||
|
||||
## Automatically set up focus neighbours when the responses list changes.
|
||||
@export var auto_configure_focus: bool = true
|
||||
|
||||
## Automatically focus the first item when showing.
|
||||
@export var auto_focus_first_item: bool = true
|
||||
|
||||
## Hide any responses where [code]is_allowed[/code] is false
|
||||
@export var hide_failed_responses: bool = false
|
||||
|
||||
## The list of dialogue responses.
|
||||
var responses: Array = []:
|
||||
set(value):
|
||||
responses = value
|
||||
_apply_responses()
|
||||
get:
|
||||
return responses
|
||||
|
||||
# The previously focused item in this menu.
|
||||
var _previously_focused_item: Control = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visibility_changed.connect(func():
|
||||
if auto_focus_first_item and visible and get_menu_items().size() > 0:
|
||||
var first_item: Control = get_menu_items()[0]
|
||||
if first_item.is_inside_tree():
|
||||
first_item.grab_focus()
|
||||
)
|
||||
|
||||
if is_instance_valid(response_template):
|
||||
response_template.hide()
|
||||
|
||||
get_viewport().gui_focus_changed.connect(_on_focus_changed)
|
||||
|
||||
|
||||
## Get the selectable items in the menu.
|
||||
func get_menu_items() -> Array:
|
||||
var items: Array = []
|
||||
for child in get_children():
|
||||
if not child.visible: continue
|
||||
if "Disallowed" in child.name: continue
|
||||
items.append(child)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
## Prepare the menu for keyboard and mouse navigation.
|
||||
func configure_focus() -> void:
|
||||
var items = get_menu_items()
|
||||
for i in items.size():
|
||||
var item: Control = items[i]
|
||||
|
||||
item.focus_mode = Control.FOCUS_ALL
|
||||
|
||||
item.focus_neighbor_left = item.get_path()
|
||||
item.focus_neighbor_right = item.get_path()
|
||||
|
||||
if i == 0:
|
||||
item.focus_neighbor_top = item.get_path()
|
||||
item.focus_neighbor_left = item.get_path()
|
||||
item.focus_previous = item.get_path()
|
||||
else:
|
||||
item.focus_neighbor_top = items[i - 1].get_path()
|
||||
item.focus_neighbor_left = items[i - 1].get_path()
|
||||
item.focus_previous = items[i - 1].get_path()
|
||||
|
||||
if i == items.size() - 1:
|
||||
item.focus_neighbor_bottom = item.get_path()
|
||||
item.focus_neighbor_right = item.get_path()
|
||||
item.focus_next = item.get_path()
|
||||
else:
|
||||
item.focus_neighbor_bottom = items[i + 1].get_path()
|
||||
item.focus_neighbor_right = items[i + 1].get_path()
|
||||
item.focus_next = items[i + 1].get_path()
|
||||
|
||||
item.mouse_entered.connect(_on_response_mouse_entered.bind(item))
|
||||
item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response")))
|
||||
|
||||
_previously_focused_item = items[0]
|
||||
|
||||
if auto_focus_first_item:
|
||||
items[0].grab_focus()
|
||||
|
||||
|
||||
#region Internal
|
||||
|
||||
|
||||
# Set up the visual side of things.
|
||||
func _apply_responses() -> void:
|
||||
# Remove any current items
|
||||
for item in get_children():
|
||||
if item == response_template: continue
|
||||
|
||||
remove_child(item)
|
||||
item.queue_free()
|
||||
|
||||
# Add new items
|
||||
if responses.size() > 0:
|
||||
for response in responses:
|
||||
if hide_failed_responses and not response.is_allowed: continue
|
||||
|
||||
var item: Control
|
||||
if is_instance_valid(response_template):
|
||||
item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS)
|
||||
item.show()
|
||||
else:
|
||||
item = Button.new()
|
||||
item.name = "Response%d" % get_child_count()
|
||||
if not response.is_allowed:
|
||||
item.name = item.name + &"Disallowed"
|
||||
item.disabled = true
|
||||
|
||||
# If the item has a response property then use that
|
||||
if "response" in item:
|
||||
item.response = response
|
||||
# Otherwise assume we can just set the text
|
||||
else:
|
||||
item.text = response.text
|
||||
|
||||
item.set_meta("response", response)
|
||||
|
||||
add_child(item)
|
||||
|
||||
if auto_configure_focus:
|
||||
configure_focus()
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_focus_changed(control: Control) -> void:
|
||||
if "Disallowed" in control.name: return
|
||||
if not control in get_menu_items(): return
|
||||
|
||||
if _previously_focused_item != control:
|
||||
_previously_focused_item = control
|
||||
response_focused.emit(control)
|
||||
|
||||
|
||||
func _on_response_mouse_entered(item: Control) -> void:
|
||||
if "Disallowed" in item.name: return
|
||||
|
||||
item.grab_focus()
|
||||
|
||||
|
||||
func _on_response_gui_input(event: InputEvent, item: Control, response) -> void:
|
||||
if "Disallowed" in item.name: return
|
||||
|
||||
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
get_viewport().set_input_as_handled()
|
||||
response_selected.emit(response)
|
||||
elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items():
|
||||
get_viewport().set_input_as_handled()
|
||||
response_selected.emit(response)
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb52rsfwhkxbn
|
||||
@@ -0,0 +1,55 @@
|
||||
class_name DMTranslationParserPlugin extends EditorTranslationParserPlugin
|
||||
|
||||
|
||||
## Cached result of parsing a dialogue file.
|
||||
var data: DMCompilerResult
|
||||
## List of characters that were added.
|
||||
var translated_character_names: PackedStringArray = []
|
||||
var translated_lines: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _parse_file(path: String) -> Array[PackedStringArray]:
|
||||
var msgs: Array[PackedStringArray] = []
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
||||
var text: String = file.get_as_text()
|
||||
|
||||
data = DMCompiler.compile_string(text, path)
|
||||
|
||||
var known_keys: PackedStringArray = PackedStringArray([])
|
||||
|
||||
# Add all character names if settings ask for it
|
||||
if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, true):
|
||||
translated_character_names = [] as Array[DialogueLine]
|
||||
for character_name: String in data.character_names:
|
||||
if character_name in known_keys: continue
|
||||
|
||||
known_keys.append(character_name)
|
||||
|
||||
translated_character_names.append(character_name)
|
||||
msgs.append(PackedStringArray([character_name.replace('"', '\"'), "dialogue", "", DMConstants.translate("translation_plugin.character_name")]))
|
||||
|
||||
# Add all dialogue lines and responses
|
||||
var dialogue: Dictionary = data.lines
|
||||
for key: String in dialogue.keys():
|
||||
var line: Dictionary = dialogue.get(key)
|
||||
|
||||
if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue
|
||||
|
||||
var translation_key: String = line.get(&"translation_key", line.text)
|
||||
if translation_key.is_empty():
|
||||
translation_key = line.text
|
||||
|
||||
if translation_key in known_keys: continue
|
||||
|
||||
known_keys.append(translation_key)
|
||||
translated_lines.append(line)
|
||||
if translation_key == line.text:
|
||||
msgs.append(PackedStringArray([line.text.replace('"', '\"'), "", "", line.get("notes", "")]))
|
||||
else:
|
||||
msgs.append(PackedStringArray([line.text.replace('"', '\"'), line.translation_key.replace('"', '\"'), "", line.get("notes", "")]))
|
||||
|
||||
return msgs
|
||||
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return ["dialogue"]
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6bya881h1egb
|
||||
@@ -0,0 +1,267 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
public partial class ExampleBalloon : CanvasLayer
|
||||
{
|
||||
[Export] public Resource DialogueResource;
|
||||
[Export] public string StartFromTitle = "";
|
||||
[Export] public bool AutoStart = false;
|
||||
[Export] public string NextAction = "ui_accept";
|
||||
[Export] public string SkipAction = "ui_cancel";
|
||||
|
||||
|
||||
Control balloon;
|
||||
RichTextLabel characterLabel;
|
||||
RichTextLabel dialogueLabel;
|
||||
VBoxContainer responsesMenu;
|
||||
Polygon2D progress;
|
||||
|
||||
Array<Variant> temporaryGameStates = new Array<Variant>();
|
||||
bool isWaitingForInput = false;
|
||||
bool willHideBalloon = false;
|
||||
|
||||
DialogueLine dialogueLine;
|
||||
DialogueLine DialogueLine
|
||||
{
|
||||
get => dialogueLine;
|
||||
set
|
||||
{
|
||||
// Dialogue has finished so close the balloon
|
||||
if (value == null)
|
||||
{
|
||||
if (Owner == null)
|
||||
{
|
||||
QueueFree();
|
||||
}
|
||||
else
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
dialogueLine = value;
|
||||
ApplyDialogueLine();
|
||||
}
|
||||
}
|
||||
|
||||
Timer MutationCooldown = new Timer();
|
||||
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
balloon = GetNode<Control>("%Balloon");
|
||||
characterLabel = GetNode<RichTextLabel>("%CharacterLabel");
|
||||
dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel");
|
||||
responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu");
|
||||
progress = GetNode<Polygon2D>("%Progress");
|
||||
|
||||
balloon.Hide();
|
||||
|
||||
balloon.GuiInput += (@event) =>
|
||||
{
|
||||
if ((bool)dialogueLabel.Get("is_typing"))
|
||||
{
|
||||
bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed();
|
||||
bool skipButtonWasPressed = @event.IsActionPressed(SkipAction);
|
||||
if (mouseWasClicked || skipButtonWasPressed)
|
||||
{
|
||||
GetViewport().SetInputAsHandled();
|
||||
dialogueLabel.Call("skip_typing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isWaitingForInput) return;
|
||||
if (dialogueLine.Responses.Count > 0) return;
|
||||
|
||||
GetViewport().SetInputAsHandled();
|
||||
|
||||
if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
Next(dialogueLine.NextId);
|
||||
}
|
||||
else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon)
|
||||
{
|
||||
Next(dialogueLine.NextId);
|
||||
}
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action")))
|
||||
{
|
||||
responsesMenu.Set("next_action", NextAction);
|
||||
}
|
||||
responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) =>
|
||||
{
|
||||
Next(response.NextId);
|
||||
}));
|
||||
|
||||
|
||||
// Hide the balloon when a mutation is running
|
||||
MutationCooldown.Timeout += () =>
|
||||
{
|
||||
if (willHideBalloon)
|
||||
{
|
||||
willHideBalloon = false;
|
||||
balloon.Hide();
|
||||
}
|
||||
};
|
||||
AddChild(MutationCooldown);
|
||||
|
||||
DialogueManager.Mutated += OnMutated;
|
||||
|
||||
if (AutoStart)
|
||||
{
|
||||
if (!IsInstanceValid(DialogueResource))
|
||||
{
|
||||
throw new System.Exception(DialogueManager.GetErrorMessage(143));
|
||||
}
|
||||
Start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
DialogueManager.Mutated -= OnMutated;
|
||||
}
|
||||
|
||||
|
||||
public override void _UnhandledInput(InputEvent @event)
|
||||
{
|
||||
// Only the balloon is allowed to handle input while it's showing
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
|
||||
|
||||
public override async void _Notification(int what)
|
||||
{
|
||||
// Detect a change of locale and update the current dialogue line to show the new language
|
||||
if (what == NotificationTranslationChanged && IsInstanceValid(dialogueLabel))
|
||||
{
|
||||
float visibleRatio = dialogueLabel.VisibleRatio;
|
||||
DialogueLine = await DialogueManager.GetNextDialogueLine(DialogueResource, DialogueLine.Id, temporaryGameStates);
|
||||
if (visibleRatio < 1.0f)
|
||||
{
|
||||
dialogueLabel.Call("skip_typing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
base._Process(delta);
|
||||
|
||||
if (IsInstanceValid(dialogueLine))
|
||||
{
|
||||
progress.Visible = !(bool)dialogueLabel.Get("is_typing") && dialogueLine.Responses.Count == 0 && !dialogueLine.HasTag("voice");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async void Start(Resource dialogueResource = null, string title = "", Array<Variant> extraGameStates = null)
|
||||
{
|
||||
temporaryGameStates = new Array<Variant> { this } + (extraGameStates ?? new Array<Variant>());
|
||||
isWaitingForInput = false;
|
||||
|
||||
if (IsInstanceValid(dialogueResource))
|
||||
{
|
||||
DialogueResource = dialogueResource;
|
||||
}
|
||||
if (title != "")
|
||||
{
|
||||
StartFromTitle = title;
|
||||
}
|
||||
|
||||
DialogueLine = await DialogueManager.GetNextDialogueLine(DialogueResource, StartFromTitle, temporaryGameStates);
|
||||
Show();
|
||||
}
|
||||
|
||||
|
||||
public async void Next(string nextId)
|
||||
{
|
||||
DialogueLine = await DialogueManager.GetNextDialogueLine(DialogueResource, nextId, temporaryGameStates);
|
||||
}
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
private async void ApplyDialogueLine()
|
||||
{
|
||||
MutationCooldown.Stop();
|
||||
|
||||
isWaitingForInput = false;
|
||||
balloon.FocusMode = Control.FocusModeEnum.All;
|
||||
balloon.GrabFocus();
|
||||
|
||||
// Set up the character name
|
||||
characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character);
|
||||
characterLabel.Text = Tr(dialogueLine.Character, "dialogue");
|
||||
|
||||
// Set up the dialogue
|
||||
dialogueLabel.Hide();
|
||||
dialogueLabel.Set("dialogue_line", dialogueLine);
|
||||
|
||||
// Set up the responses
|
||||
responsesMenu.Hide();
|
||||
responsesMenu.Set("responses", dialogueLine.Responses);
|
||||
|
||||
// Type out the text
|
||||
balloon.Show();
|
||||
willHideBalloon = false;
|
||||
dialogueLabel.Show();
|
||||
if (!string.IsNullOrEmpty(dialogueLine.Text))
|
||||
{
|
||||
dialogueLabel.Call("type_out");
|
||||
await ToSignal(dialogueLabel, "finished_typing");
|
||||
}
|
||||
|
||||
// Wait for input
|
||||
if (dialogueLine.Responses.Count > 0)
|
||||
{
|
||||
balloon.FocusMode = Control.FocusModeEnum.None;
|
||||
responsesMenu.Show();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(dialogueLine.Time))
|
||||
{
|
||||
float time = 0f;
|
||||
if (!float.TryParse(dialogueLine.Time, out time))
|
||||
{
|
||||
time = dialogueLine.Text.Length * 0.02f;
|
||||
}
|
||||
await ToSignal(GetTree().CreateTimer(time), "timeout");
|
||||
Next(dialogueLine.NextId);
|
||||
}
|
||||
else
|
||||
{
|
||||
isWaitingForInput = true;
|
||||
balloon.FocusMode = Control.FocusModeEnum.All;
|
||||
balloon.GrabFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region signals
|
||||
|
||||
|
||||
private void OnMutated(Dictionary mutation)
|
||||
{
|
||||
if (!(bool)mutation["is_inline"])
|
||||
{
|
||||
isWaitingForInput = false;
|
||||
willHideBalloon = true;
|
||||
MutationCooldown.Start(0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://5b3w40kwakl3
|
||||
@@ -0,0 +1,217 @@
|
||||
class_name DialogueManagerExampleBalloon extends CanvasLayer
|
||||
## A basic dialogue balloon for use with Dialogue Manager.
|
||||
|
||||
|
||||
## The dialogue resource
|
||||
@export var dialogue_resource: DialogueResource
|
||||
|
||||
## Start from a given title when using balloon as a [Node] in a scene.
|
||||
@export var start_from_title: String = ""
|
||||
|
||||
## If running as a [Node] in a scene then auto start the dialogue.
|
||||
@export var auto_start: bool = false
|
||||
|
||||
## If all other input is blocked as long as dialogue is shown.
|
||||
@export var will_block_other_input: bool = true
|
||||
|
||||
## The action to use for advancing the dialogue
|
||||
@export var next_action: StringName = &"ui_accept"
|
||||
|
||||
## The action to use to skip typing the dialogue
|
||||
@export var skip_action: StringName = &"ui_cancel"
|
||||
|
||||
## A sound player for voice lines (if they exist).
|
||||
@onready var audio_stream_player: AudioStreamPlayer = %AudioStreamPlayer
|
||||
|
||||
## Temporary game states
|
||||
var temporary_game_states: Array = []
|
||||
|
||||
## See if we are waiting for the player
|
||||
var is_waiting_for_input: bool = false
|
||||
|
||||
## See if we are running a long mutation and should hide the balloon
|
||||
var will_hide_balloon: bool = false
|
||||
|
||||
## A dictionary to store any ephemeral variables
|
||||
var locals: Dictionary = {}
|
||||
|
||||
var _locale: String = TranslationServer.get_locale()
|
||||
|
||||
## The current line
|
||||
var dialogue_line: DialogueLine:
|
||||
set(value):
|
||||
if value:
|
||||
dialogue_line = value
|
||||
apply_dialogue_line()
|
||||
else:
|
||||
# The dialogue has finished so close the balloon
|
||||
if owner == null:
|
||||
queue_free()
|
||||
else:
|
||||
hide()
|
||||
get:
|
||||
return dialogue_line
|
||||
|
||||
## A cooldown timer for delaying the balloon hide when encountering a mutation.
|
||||
var mutation_cooldown: Timer = Timer.new()
|
||||
|
||||
## The base balloon anchor
|
||||
@onready var balloon: Control = %Balloon
|
||||
|
||||
## The label showing the name of the currently speaking character
|
||||
@onready var character_label: RichTextLabel = %CharacterLabel
|
||||
|
||||
## The label showing the currently spoken dialogue
|
||||
@onready var dialogue_label: DialogueLabel = %DialogueLabel
|
||||
|
||||
## The menu of responses
|
||||
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
|
||||
|
||||
## Indicator to show that player can progress dialogue.
|
||||
@onready var progress: Polygon2D = %Progress
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
balloon.hide()
|
||||
Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated)
|
||||
|
||||
# If the responses menu doesn't have a next action set, use this one
|
||||
if responses_menu.next_action.is_empty():
|
||||
responses_menu.next_action = next_action
|
||||
|
||||
mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout)
|
||||
add_child(mutation_cooldown)
|
||||
|
||||
if auto_start:
|
||||
if not is_instance_valid(dialogue_resource):
|
||||
assert(false, DMConstants.get_error_message(DMConstants.ERR_MISSING_RESOURCE_FOR_AUTOSTART))
|
||||
start()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if is_instance_valid(dialogue_line):
|
||||
progress.visible = not dialogue_label.is_typing and dialogue_line.responses.size() == 0 and not dialogue_line.has_tag("voice")
|
||||
|
||||
|
||||
func _unhandled_input(_event: InputEvent) -> void:
|
||||
# Only the balloon is allowed to handle input while it's showing
|
||||
if will_block_other_input:
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
## Detect a change of locale and update the current dialogue line to show the new language
|
||||
if what == NOTIFICATION_TRANSLATION_CHANGED and _locale != TranslationServer.get_locale() and is_instance_valid(dialogue_label):
|
||||
_locale = TranslationServer.get_locale()
|
||||
var visible_ratio: float = dialogue_label.visible_ratio
|
||||
dialogue_line = await dialogue_resource.get_next_dialogue_line(dialogue_line.id)
|
||||
if visible_ratio < 1:
|
||||
dialogue_label.skip_typing()
|
||||
|
||||
|
||||
## Start some dialogue
|
||||
func start(with_dialogue_resource: DialogueResource = null, title: String = "", extra_game_states: Array = []) -> void:
|
||||
temporary_game_states = [self] + extra_game_states
|
||||
is_waiting_for_input = false
|
||||
if is_instance_valid(with_dialogue_resource):
|
||||
dialogue_resource = with_dialogue_resource
|
||||
if not title.is_empty():
|
||||
start_from_title = title
|
||||
dialogue_line = await dialogue_resource.get_next_dialogue_line(start_from_title, temporary_game_states)
|
||||
show()
|
||||
|
||||
|
||||
## Apply any changes to the balloon given a new [DialogueLine].
|
||||
func apply_dialogue_line() -> void:
|
||||
mutation_cooldown.stop()
|
||||
|
||||
progress.hide()
|
||||
is_waiting_for_input = false
|
||||
balloon.focus_mode = Control.FOCUS_ALL
|
||||
balloon.grab_focus()
|
||||
|
||||
character_label.visible = not dialogue_line.character.is_empty()
|
||||
character_label.text = tr(dialogue_line.character, "dialogue")
|
||||
|
||||
dialogue_label.hide()
|
||||
dialogue_label.dialogue_line = dialogue_line
|
||||
|
||||
responses_menu.hide()
|
||||
responses_menu.responses = dialogue_line.responses
|
||||
|
||||
# Show our balloon
|
||||
balloon.show()
|
||||
will_hide_balloon = false
|
||||
|
||||
dialogue_label.show()
|
||||
if not dialogue_line.text.is_empty():
|
||||
dialogue_label.type_out()
|
||||
await dialogue_label.finished_typing
|
||||
|
||||
# Wait for next line
|
||||
if dialogue_line.has_tag("voice"):
|
||||
audio_stream_player.stream = load(dialogue_line.get_tag_value("voice"))
|
||||
audio_stream_player.play()
|
||||
await audio_stream_player.finished
|
||||
next(dialogue_line.next_id)
|
||||
elif dialogue_line.responses.size() > 0:
|
||||
balloon.focus_mode = Control.FOCUS_NONE
|
||||
responses_menu.show()
|
||||
elif dialogue_line.time != "":
|
||||
var time: float = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float()
|
||||
await get_tree().create_timer(time).timeout
|
||||
next(dialogue_line.next_id)
|
||||
else:
|
||||
is_waiting_for_input = true
|
||||
balloon.focus_mode = Control.FOCUS_ALL
|
||||
balloon.grab_focus()
|
||||
|
||||
|
||||
## Go to the next line
|
||||
func next(next_id: String) -> void:
|
||||
dialogue_line = await dialogue_resource.get_next_dialogue_line(next_id, temporary_game_states)
|
||||
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_mutation_cooldown_timeout() -> void:
|
||||
if will_hide_balloon:
|
||||
will_hide_balloon = false
|
||||
balloon.hide()
|
||||
|
||||
|
||||
func _on_mutated(mutation: Dictionary) -> void:
|
||||
if not mutation.is_inline:
|
||||
is_waiting_for_input = false
|
||||
will_hide_balloon = true
|
||||
mutation_cooldown.start(0.1)
|
||||
|
||||
|
||||
func _on_balloon_gui_input(event: InputEvent) -> void:
|
||||
# See if we need to skip typing of the dialogue
|
||||
if dialogue_label.is_typing:
|
||||
var mouse_was_clicked: bool = event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed()
|
||||
var skip_button_was_pressed: bool = event.is_action_pressed(skip_action)
|
||||
if mouse_was_clicked or skip_button_was_pressed:
|
||||
get_viewport().set_input_as_handled()
|
||||
dialogue_label.skip_typing()
|
||||
return
|
||||
|
||||
if not is_waiting_for_input: return
|
||||
if dialogue_line.responses.size() > 0: return
|
||||
|
||||
# When there are no response options the balloon itself is the clickable thing
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
next(dialogue_line.next_id)
|
||||
elif event.is_action_pressed(next_action) and get_viewport().gui_get_focus_owner() == balloon:
|
||||
next(dialogue_line.next_id)
|
||||
|
||||
|
||||
func _on_responses_menu_response_selected(response: DialogueResponse) -> void:
|
||||
next(response.next_id)
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1wt4ma6055l8
|
||||
@@ -0,0 +1,198 @@
|
||||
[gd_scene format=3 uid="uid://73jm5qjy52vq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_36de5"]
|
||||
[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"]
|
||||
[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_72ixx"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(0.329412, 0.329412, 0.329412, 1)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ri4m3"]
|
||||
bg_color = Color(0.121569, 0.121569, 0.121569, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0njw"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(0.6, 0.6, 0.6, 1)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qkmqt"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="Theme" id="Theme_qq3yp"]
|
||||
default_font_size = 20
|
||||
Button/styles/disabled = SubResource("StyleBoxFlat_spyqn")
|
||||
Button/styles/focus = SubResource("StyleBoxFlat_ri4m3")
|
||||
Button/styles/hover = SubResource("StyleBoxFlat_e0njw")
|
||||
Button/styles/normal = SubResource("StyleBoxFlat_e0njw")
|
||||
MarginContainer/constants/margin_bottom = 15
|
||||
MarginContainer/constants/margin_left = 30
|
||||
MarginContainer/constants/margin_right = 30
|
||||
MarginContainer/constants/margin_top = 15
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt")
|
||||
|
||||
[sub_resource type="Animation" id="Animation_nlsx6"]
|
||||
length = 0.001
|
||||
tracks/0/type = "bezier"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Progress:position:y")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"handle_modes": PackedInt32Array(0),
|
||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0),
|
||||
"times": PackedFloat32Array(0)
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_qkmqt"]
|
||||
resource_name = "progress"
|
||||
loop_mode = 1
|
||||
step = 0.1
|
||||
tracks/0/type = "bezier"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Progress:position:y")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"handle_modes": PackedInt32Array(0, 0, 0, 0, 0),
|
||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0, 0, -0.25, 0, 0.2, -0.0157438, -4, -0.2, 0.0112069, 0.25, 0, -4, -0.25, 0, 0.25, 0, 0, -0.2, 0.00299701, 0.25, 0),
|
||||
"times": PackedFloat32Array(0, 0.1, 0.5, 0.6, 1)
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_1337t"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_nlsx6"),
|
||||
&"progress": SubResource("Animation_qkmqt")
|
||||
}
|
||||
|
||||
[node name="ExampleBalloon" type="CanvasLayer" unique_id=1434168376]
|
||||
layer = 100
|
||||
script = ExtResource("1_36de5")
|
||||
|
||||
[node name="Balloon" type="Control" parent="." unique_id=359403728]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = SubResource("Theme_qq3yp")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon" unique_id=1011109179]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -219.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer" unique_id=810051587]
|
||||
clip_children = 2
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer" unique_id=1030936092]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer" unique_id=1428277592]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer" unique_id=1624289611]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/VBoxContainer" unique_id=507199029]
|
||||
unique_name_in_owner = true
|
||||
modulate = Color(1, 1, 1, 0.501961)
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
text = "Character"
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
|
||||
[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1225160028 instance=ExtResource("2_a8ve6")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
text = "Dialogue..."
|
||||
|
||||
[node name="Control" type="Control" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer" unique_id=1162966119]
|
||||
custom_minimum_size = Vector2(20, 10)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 8
|
||||
|
||||
[node name="Progress" type="Polygon2D" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/Control" unique_id=821575988]
|
||||
unique_name_in_owner = true
|
||||
polygon = PackedVector2Array(0, 0, 10, 10, 20, 0)
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/Control" unique_id=452325722]
|
||||
libraries/ = SubResource("AnimationLibrary_1337t")
|
||||
autoplay = &"progress"
|
||||
|
||||
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon" unique_id=966297223 node_paths=PackedStringArray("response_template")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -290.5
|
||||
offset_top = -35.0
|
||||
offset_right = 290.5
|
||||
offset_bottom = 35.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 8
|
||||
theme_override_constants/separation = 2
|
||||
alignment = 1
|
||||
script = ExtResource("3_72ixx")
|
||||
response_template = NodePath("ResponseExample")
|
||||
|
||||
[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu" unique_id=914537232]
|
||||
layout_mode = 2
|
||||
text = "Response example"
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=1116280512]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
|
||||
[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]
|
||||
@@ -0,0 +1,210 @@
|
||||
[gd_scene load_steps=12 format=3 uid="uid://13s5spsk34qu"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_s2gbs"]
|
||||
[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"]
|
||||
[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_1j1j0"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.345098, 0.345098, 0.345098, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ufjut"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.227451, 0.227451, 0.227451, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fcbqo"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t6i7a"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="Theme" id="Theme_qq3yp"]
|
||||
default_font_size = 8
|
||||
Button/styles/disabled = SubResource("StyleBoxFlat_235ry")
|
||||
Button/styles/focus = SubResource("StyleBoxFlat_ufjut")
|
||||
Button/styles/hover = SubResource("StyleBoxFlat_fcbqo")
|
||||
Button/styles/normal = SubResource("StyleBoxFlat_t6i7a")
|
||||
MarginContainer/constants/margin_bottom = 4
|
||||
MarginContainer/constants/margin_left = 8
|
||||
MarginContainer/constants/margin_right = 8
|
||||
MarginContainer/constants/margin_top = 4
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm")
|
||||
|
||||
[sub_resource type="Animation" id="Animation_i6nbm"]
|
||||
resource_name = "progress"
|
||||
loop_mode = 1
|
||||
step = 0.1
|
||||
tracks/0/type = "bezier"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Progress:position:y")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"handle_modes": PackedInt32Array(0, 0, 0, 0, 0),
|
||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0, 0, -0.25, 0, 0.2, -0.0157438, -1, -0.2, 0.0112069, 0.25, 0, -1, -0.25, 0, 0.25, 0, 0, -0.2, 0.00299701, 0.25, 0),
|
||||
"times": PackedFloat32Array(0, 0.1, 0.5, 0.6, 1)
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_6b6c8"]
|
||||
_data = {
|
||||
&"progress": SubResource("Animation_i6nbm")
|
||||
}
|
||||
|
||||
[node name="ExampleBalloon" type="CanvasLayer"]
|
||||
layer = 100
|
||||
script = ExtResource("1_s2gbs")
|
||||
|
||||
[node name="Balloon" type="Control" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = SubResource("Theme_qq3yp")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -71.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"]
|
||||
clip_children = 2
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
modulate = Color(1, 1, 1, 0.501961)
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
text = "Character"
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
|
||||
[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/VBoxContainer" instance=ExtResource("2_hfvdi")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
text = "Dialogue..."
|
||||
|
||||
[node name="Control" type="Control" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(5, 3)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 8
|
||||
|
||||
[node name="Progress" type="Polygon2D" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/Control"]
|
||||
unique_name_in_owner = true
|
||||
position = Vector2(0, -0.455998)
|
||||
polygon = PackedVector2Array(0, 0, 2.5, 3, 5, 0)
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/Control"]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_6b6c8")
|
||||
}
|
||||
autoplay = "progress"
|
||||
|
||||
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -116.5
|
||||
offset_top = -9.0
|
||||
offset_right = 116.5
|
||||
offset_bottom = 9.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 8
|
||||
theme_override_constants/separation = 2
|
||||
script = ExtResource("3_1j1j0")
|
||||
|
||||
[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"]
|
||||
layout_mode = 2
|
||||
text = "Response Example"
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
|
||||
[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]
|
||||
@@ -0,0 +1,26 @@
|
||||
class_name DMExportPlugin extends EditorExportPlugin
|
||||
|
||||
const IGNORED_PATHS = [
|
||||
"/assets",
|
||||
"/components",
|
||||
"/views",
|
||||
"inspector_plugin",
|
||||
"test_scene"
|
||||
]
|
||||
|
||||
|
||||
func _get_name() -> String:
|
||||
return "Dialogue Manager Export Plugin"
|
||||
|
||||
|
||||
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
|
||||
var plugin_path: String = DMPlugin.get_plugin_path()
|
||||
|
||||
# Ignore any editor stuff
|
||||
for ignored_path: String in IGNORED_PATHS:
|
||||
if path.begins_with(plugin_path + ignored_path):
|
||||
skip()
|
||||
|
||||
# Ignore C# stuff it not using dotnet
|
||||
if path.begins_with(plugin_path) and not DMSettings.check_for_dotnet_solution() and path.ends_with(".cs"):
|
||||
skip()
|
||||
@@ -0,0 +1 @@
|
||||
uid://sa55ra11ji2q
|
||||
@@ -0,0 +1,108 @@
|
||||
@tool
|
||||
class_name DMImportPlugin extends EditorImportPlugin
|
||||
|
||||
|
||||
signal compiled_resource(resource: Resource)
|
||||
|
||||
|
||||
const COMPILER_VERSION = 15
|
||||
|
||||
|
||||
func _get_importer_name() -> String:
|
||||
return "dialogue_manager"
|
||||
|
||||
|
||||
func _get_format_version() -> int:
|
||||
return COMPILER_VERSION
|
||||
|
||||
|
||||
func _get_visible_name() -> String:
|
||||
return "Dialogue"
|
||||
|
||||
|
||||
func _get_import_order() -> int:
|
||||
return -1000
|
||||
|
||||
|
||||
func _get_priority() -> float:
|
||||
return 1000.0
|
||||
|
||||
|
||||
func _get_resource_type():
|
||||
return "Resource"
|
||||
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return PackedStringArray(["dialogue"])
|
||||
|
||||
|
||||
func _get_save_extension():
|
||||
return "tres"
|
||||
|
||||
|
||||
func _get_preset_count() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
func _get_preset_name(preset_index: int) -> String:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
func _get_import_options(path: String, preset_index: int) -> Array:
|
||||
# When the options array is empty there is a misleading error on export
|
||||
# that actually means nothing so let's just have an invisible option.
|
||||
return [{
|
||||
name = "defaults",
|
||||
default_value = true
|
||||
}]
|
||||
|
||||
|
||||
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error:
|
||||
# Get the raw file contents
|
||||
if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND
|
||||
|
||||
var file: FileAccess = FileAccess.open(source_file, FileAccess.READ)
|
||||
var raw_text: String = file.get_as_text()
|
||||
|
||||
DMPlugin.instance.cache_file_content_changed.emit(source_file, raw_text)
|
||||
|
||||
# Compile the text
|
||||
var result: DMCompilerResult = DMCompiler.compile_string(raw_text, source_file)
|
||||
if result.errors.size() > 0:
|
||||
printerr("%d errors found in %s" % [result.errors.size(), source_file])
|
||||
DMCache.add_errors_to_file(source_file, result.errors)
|
||||
return OK
|
||||
|
||||
# Get the current addon version
|
||||
var config: ConfigFile = ConfigFile.new()
|
||||
config.load("res://addons/dialogue_manager/plugin.cfg")
|
||||
var version: String = config.get_value("plugin", "version")
|
||||
|
||||
# Save the results to a resource
|
||||
var resource: DialogueResource = DialogueResource.new()
|
||||
resource.set_meta("dialogue_manager_version", version)
|
||||
|
||||
resource.using_states = result.using_states
|
||||
resource.titles = result.titles
|
||||
resource.first_title = result.first_title
|
||||
resource.character_names = result.character_names
|
||||
resource.lines = result.lines
|
||||
resource.raw_text = result.raw_text
|
||||
|
||||
# Clear errors and possibly trigger any cascade recompiles
|
||||
DMCache.add_file(source_file, result)
|
||||
|
||||
var err: Error = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()])
|
||||
|
||||
compiled_resource.emit(resource)
|
||||
|
||||
# Recompile any dependencies
|
||||
var dependent_paths: PackedStringArray = DMCache.get_dependent_paths_for_reimport(source_file)
|
||||
for path in dependent_paths:
|
||||
append_import_external_resource(path)
|
||||
|
||||
return err
|
||||
@@ -0,0 +1 @@
|
||||
uid://dhwpj6ed8soyq
|
||||
@@ -0,0 +1,17 @@
|
||||
@tool
|
||||
class_name DMInspectorPlugin extends EditorInspectorPlugin
|
||||
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
if object is GDScript: return false
|
||||
if not object is Node and not object is Resource: return false
|
||||
if "name" in object and object.name == "Dialogue Manager": return false
|
||||
return true
|
||||
|
||||
|
||||
func _parse_property(object: Object, type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
|
||||
if hint_string == "DialogueResource" or ("dialogue" in name.to_lower() and hint_string == "Resource"):
|
||||
add_property_editor(name, DMDialogueEditorProperty.new())
|
||||
return true
|
||||
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://0x31sbqbikov
|
||||
@@ -0,0 +1,466 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Dialogue Manager\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 3.2.2\n"
|
||||
|
||||
msgid "start_a_new_file"
|
||||
msgstr "Start a new file"
|
||||
|
||||
msgid "open_a_file"
|
||||
msgstr "Open a file"
|
||||
|
||||
msgid "open.open"
|
||||
msgstr "Open..."
|
||||
|
||||
msgid "open.quick_open"
|
||||
msgstr "Quick open..."
|
||||
|
||||
msgid "open.no_recent_files"
|
||||
msgstr "No recent files"
|
||||
|
||||
msgid "open.clear_recent_files"
|
||||
msgstr "Clear recent files"
|
||||
|
||||
msgid "save_all_files"
|
||||
msgstr "Save all files"
|
||||
|
||||
msgid "all"
|
||||
msgstr "All"
|
||||
|
||||
msgid "find_in_files"
|
||||
msgstr "Find in Dialogue..."
|
||||
|
||||
msgid "test_dialogue"
|
||||
msgstr "Test dialogue from start of file"
|
||||
|
||||
msgid "test_dialogue_from_line"
|
||||
msgstr "Test dialogue from current line"
|
||||
|
||||
msgid "search_for_text"
|
||||
msgstr "Search for text"
|
||||
|
||||
msgid "insert"
|
||||
msgstr "Insert"
|
||||
|
||||
msgid "translations"
|
||||
msgstr "Translations"
|
||||
|
||||
msgid "sponsor"
|
||||
msgstr "Sponsor"
|
||||
|
||||
msgid "show_support"
|
||||
msgstr "Support Dialogue Manager"
|
||||
|
||||
msgid "docs"
|
||||
msgstr "Docs"
|
||||
|
||||
msgid "insert.wave_bbcode"
|
||||
msgstr "Wave BBCode"
|
||||
|
||||
msgid "insert.shake_bbcode"
|
||||
msgstr "Shake BBCode"
|
||||
|
||||
msgid "insert.typing_pause"
|
||||
msgstr "Typing pause"
|
||||
|
||||
msgid "insert.typing_speed_change"
|
||||
msgstr "Typing speed change"
|
||||
|
||||
msgid "insert.auto_advance"
|
||||
msgstr "Auto advance"
|
||||
|
||||
msgid "insert.templates"
|
||||
msgstr "Templates"
|
||||
|
||||
msgid "insert.title"
|
||||
msgstr "Title"
|
||||
|
||||
msgid "insert.dialogue"
|
||||
msgstr "Dialogue"
|
||||
|
||||
msgid "insert.response"
|
||||
msgstr "Response"
|
||||
|
||||
msgid "insert.random_lines"
|
||||
msgstr "Random lines"
|
||||
|
||||
msgid "insert.random_text"
|
||||
msgstr "Random text"
|
||||
|
||||
msgid "insert.actions"
|
||||
msgstr "Actions"
|
||||
|
||||
msgid "insert.jump"
|
||||
msgstr "Jump to title"
|
||||
|
||||
msgid "insert.end_dialogue"
|
||||
msgstr "End dialogue"
|
||||
|
||||
msgid "generate_line_ids_for_project"
|
||||
msgstr "Generate line IDs for project"
|
||||
|
||||
msgid "generate_line_ids_for_file"
|
||||
msgstr "Generate line IDs for file"
|
||||
|
||||
msgid "generate_ids.warning_title"
|
||||
msgstr "Generate line IDs?"
|
||||
|
||||
msgid "generate_ids.warning_text"
|
||||
msgstr "Generate line IDs for all lines in all dialogue files?\n\n**Make sure to commit any changes to source control BEFORE running this because it cannot be undone.**"
|
||||
|
||||
msgid "generate_ids.ok_button"
|
||||
msgstr "Generate IDs"
|
||||
|
||||
msgid "use_uuid_only_for_ids"
|
||||
msgstr "Use UUID only for IDs"
|
||||
|
||||
msgid "set_id_prefix_length"
|
||||
msgstr "Set ID prefix length"
|
||||
|
||||
msgid "id_prefix_length"
|
||||
msgstr "ID prefix length:"
|
||||
|
||||
msgid "save_characters_to_csv"
|
||||
msgstr "Save character names to CSV..."
|
||||
|
||||
msgid "save_to_csv"
|
||||
msgstr "Save lines to CSV..."
|
||||
|
||||
msgid "import_from_csv"
|
||||
msgstr "Import line changes from CSV..."
|
||||
|
||||
msgid "confirm_close"
|
||||
msgstr "Save changes to '{path}'?"
|
||||
|
||||
msgid "confirm_close.save"
|
||||
msgstr "Save changes"
|
||||
|
||||
msgid "confirm_close.discard"
|
||||
msgstr "Discard"
|
||||
|
||||
msgid "confirm_n_unsaved_files"
|
||||
msgstr "You have {count} unsaved dialogue files."
|
||||
|
||||
msgid "buffer.save"
|
||||
msgstr "Save"
|
||||
|
||||
msgid "buffer.save_as"
|
||||
msgstr "Save as..."
|
||||
|
||||
msgid "buffer.close"
|
||||
msgstr "Close"
|
||||
|
||||
msgid "buffer.close_all"
|
||||
msgstr "Close all"
|
||||
|
||||
msgid "buffer.close_other_files"
|
||||
msgstr "Close other files"
|
||||
|
||||
msgid "buffer.copy_file_path"
|
||||
msgstr "Copy file path"
|
||||
|
||||
msgid "buffer.show_in_filesystem"
|
||||
msgstr "Show in FileSystem"
|
||||
|
||||
msgid "n_of_n"
|
||||
msgstr "{index} of {total}"
|
||||
|
||||
msgid "search.find_in_dialogue"
|
||||
msgstr "Find in Dialogue"
|
||||
|
||||
msgid "search.find"
|
||||
msgstr "Find:"
|
||||
|
||||
msgid "search.find_all"
|
||||
msgstr "Find all..."
|
||||
|
||||
msgid "search.placeholder"
|
||||
msgstr "Text to search for"
|
||||
|
||||
msgid "search.replace_placeholder"
|
||||
msgstr "Text to replace it with"
|
||||
|
||||
msgid "search.replace_selected"
|
||||
msgstr "Replace selected"
|
||||
|
||||
msgid "search.previous"
|
||||
msgstr "Previous"
|
||||
|
||||
msgid "search.next"
|
||||
msgstr "Next"
|
||||
|
||||
msgid "search.match_case"
|
||||
msgstr "Match case"
|
||||
|
||||
msgid "search.toggle_replace"
|
||||
msgstr "Replace"
|
||||
|
||||
msgid "search.replace_with"
|
||||
msgstr "Replace with:"
|
||||
|
||||
msgid "search.replace"
|
||||
msgstr "Replace"
|
||||
|
||||
msgid "search.replace_all"
|
||||
msgstr "Replace all"
|
||||
|
||||
msgid "files_list.filter"
|
||||
msgstr "Filter files"
|
||||
|
||||
msgid "titles_list.filter"
|
||||
msgstr "Filter titles"
|
||||
|
||||
msgid "errors.key_not_found"
|
||||
msgstr "Key \"{key}\" not found."
|
||||
|
||||
msgid "errors.line_and_message"
|
||||
msgstr "Error at {line}, {column}: {message}"
|
||||
|
||||
msgid "errors_in_script"
|
||||
msgstr "You have errors in your script. Fix them and then try again."
|
||||
|
||||
msgid "errors_with_build"
|
||||
msgstr "You need to fix dialogue errors before you can run your game."
|
||||
|
||||
msgid "errors.import_errors"
|
||||
msgstr "There are errors in this imported file."
|
||||
|
||||
msgid "errors.already_imported"
|
||||
msgstr "File already imported."
|
||||
|
||||
msgid "errors.duplicate_import"
|
||||
msgstr "Duplicate import name."
|
||||
|
||||
msgid "errors.unknown_using"
|
||||
msgstr "Unknown autoload in using statement."
|
||||
|
||||
msgid "errors.empty_title"
|
||||
msgstr "Titles cannot be empty."
|
||||
|
||||
msgid "errors.duplicate_title"
|
||||
msgstr "There is already a title with that name."
|
||||
|
||||
msgid "errors.invalid_title_string"
|
||||
msgstr "Titles can only contain alphanumeric characters and numbers."
|
||||
|
||||
msgid "errors.invalid_title_number"
|
||||
msgstr "Titles cannot begin with a number."
|
||||
|
||||
msgid "errors.unknown_title"
|
||||
msgstr "Unknown title."
|
||||
|
||||
msgid "errors.jump_to_invalid_title"
|
||||
msgstr "This jump is pointing to an invalid title."
|
||||
|
||||
msgid "errors.title_has_no_content"
|
||||
msgstr "That title has no content. Maybe change this to a \"=> END\"."
|
||||
|
||||
msgid "errors.invalid_expression"
|
||||
msgstr "Expression is invalid."
|
||||
|
||||
msgid "errors.unexpected_condition"
|
||||
msgstr "Unexpected condition."
|
||||
|
||||
msgid "errors.duplicate_id"
|
||||
msgstr "This ID is already in use."
|
||||
|
||||
msgid "errors.missing_id"
|
||||
msgstr "This line is missing an ID."
|
||||
|
||||
msgid "errors.invalid_indentation"
|
||||
msgstr "Invalid indentation."
|
||||
|
||||
msgid "errors.condition_has_no_content"
|
||||
msgstr "A condition line needs an indented line below it."
|
||||
|
||||
msgid "errors.incomplete_expression"
|
||||
msgstr "Incomplete expression."
|
||||
|
||||
msgid "errors.invalid_expression_for_value"
|
||||
msgstr "Invalid expression for value."
|
||||
|
||||
msgid "errors.file_not_found"
|
||||
msgstr "File not found."
|
||||
|
||||
msgid "errors.unexpected_end_of_expression"
|
||||
msgstr "Unexpected end of expression."
|
||||
|
||||
msgid "errors.unexpected_function"
|
||||
msgstr "Unexpected function."
|
||||
|
||||
msgid "errors.unexpected_bracket"
|
||||
msgstr "Unexpected bracket."
|
||||
|
||||
msgid "errors.unexpected_closing_bracket"
|
||||
msgstr "Unexpected closing bracket."
|
||||
|
||||
msgid "errors.missing_closing_bracket"
|
||||
msgstr "Missing closing bracket."
|
||||
|
||||
msgid "errors.unexpected_operator"
|
||||
msgstr "Unexpected operator."
|
||||
|
||||
msgid "errors.unexpected_comma"
|
||||
msgstr "Unexpected comma."
|
||||
|
||||
msgid "errors.unexpected_colon"
|
||||
msgstr "Unexpected colon."
|
||||
|
||||
msgid "errors.unexpected_dot"
|
||||
msgstr "Unexpected dot."
|
||||
|
||||
msgid "errors.unexpected_boolean"
|
||||
msgstr "Unexpected boolean."
|
||||
|
||||
msgid "errors.unexpected_string"
|
||||
msgstr "Unexpected string."
|
||||
|
||||
msgid "errors.unexpected_number"
|
||||
msgstr "Unexpected number."
|
||||
|
||||
msgid "errors.unexpected_variable"
|
||||
msgstr "Unexpected variable."
|
||||
|
||||
msgid "errors.invalid_index"
|
||||
msgstr "Invalid index."
|
||||
|
||||
msgid "errors.unexpected_assignment"
|
||||
msgstr "Unexpected assignment."
|
||||
|
||||
msgid "errors.expected_when_or_else"
|
||||
msgstr "Expecting a when or an else case."
|
||||
|
||||
msgid "errors.only_one_else_allowed"
|
||||
msgstr "Only one else case is allowed per match."
|
||||
|
||||
msgid "errors.when_must_belong_to_match"
|
||||
msgstr "When statements can only appear as children of match statements."
|
||||
|
||||
msgid "errors.concurrent_line_without_origin"
|
||||
msgstr "Concurrent lines need an origin line that doesn't start with \"| \"."
|
||||
|
||||
msgid "errors.goto_not_allowed_on_concurrect_lines"
|
||||
msgstr "Goto references are not allowed on concurrent dialogue lines."
|
||||
|
||||
msgid "errors.unexpected_syntax_on_nested_dialogue_line"
|
||||
msgstr "Nested dialogue lines may only contain dialogue."
|
||||
|
||||
msgid "errors.err_nested_dialogue_invalid_jump"
|
||||
msgstr "Only the last line of nested dialogue is allowed to include a jump."
|
||||
|
||||
msgid "errors.missing_resource_for_autostart"
|
||||
msgstr "You need to specify a dialogue resource when using auto-start."
|
||||
|
||||
msgid "errors.unknown"
|
||||
msgstr "Unknown syntax."
|
||||
|
||||
msgid "update.available"
|
||||
msgstr "v{version} available"
|
||||
|
||||
msgid "update.is_available_for_download"
|
||||
msgstr "Version %s is available for download!"
|
||||
|
||||
msgid "update.downloading"
|
||||
msgstr "Downloading..."
|
||||
|
||||
msgid "update.download_update"
|
||||
msgstr "Download update"
|
||||
|
||||
msgid "update.needs_reload"
|
||||
msgstr "The project needs to be reloaded to install the update."
|
||||
|
||||
msgid "update.reload_ok_button"
|
||||
msgstr "Reload project"
|
||||
|
||||
msgid "update.reload_cancel_button"
|
||||
msgstr "Do it later"
|
||||
|
||||
msgid "update.reload_project"
|
||||
msgstr "Reload project"
|
||||
|
||||
msgid "update.release_notes"
|
||||
msgstr "Read release notes"
|
||||
|
||||
msgid "update.success"
|
||||
msgstr "Dialogue Manager is now v{version}."
|
||||
|
||||
msgid "update.failed"
|
||||
msgstr "There was a problem downloading the update."
|
||||
|
||||
msgid "runtime.no_resource"
|
||||
msgstr "No dialogue resource provided."
|
||||
|
||||
msgid "runtime.no_content"
|
||||
msgstr "\"{file_path}\" has no content."
|
||||
|
||||
msgid "runtime.errors"
|
||||
msgstr "You have {count} errors in your dialogue text."
|
||||
|
||||
msgid "runtime.error_detail"
|
||||
msgstr "Line {line}: {message}"
|
||||
|
||||
msgid "runtime.errors_see_details"
|
||||
msgstr "You have {count} errors in your dialogue text. See Output for details."
|
||||
|
||||
msgid "runtime.invalid_expression"
|
||||
msgstr "\"{expression}\" is not a valid expression: {error}"
|
||||
|
||||
msgid "runtime.array_index_out_of_bounds"
|
||||
msgstr "Index {index} out of bounds of array \"{array}\"."
|
||||
|
||||
msgid "runtime.left_hand_size_cannot_be_assigned_to"
|
||||
msgstr "Left hand side of expression cannot be assigned to."
|
||||
|
||||
msgid "runtime.key_not_found"
|
||||
msgstr "Key \"{key}\" not found in dictionary \"{dictionary}\""
|
||||
|
||||
msgid "runtime.property_not_found"
|
||||
msgstr "\"{property}\" not found. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties."
|
||||
|
||||
msgid "runtime.property_not_found_missing_export"
|
||||
msgstr "\"{property}\" not found. You might need to add an [Export] decorator. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties."
|
||||
|
||||
msgid "runtime.method_not_found"
|
||||
msgstr "Method \"{method}\" not found. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties."
|
||||
|
||||
msgid "runtime.signal_not_found"
|
||||
msgstr "Signal \"{signal_name}\" not found. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties."
|
||||
|
||||
msgid "runtime.method_not_callable"
|
||||
msgstr "\"{method}\" is not a callable method on \"{object}\""
|
||||
|
||||
msgid "runtime.unknown_operator"
|
||||
msgstr "Unknown operator."
|
||||
|
||||
msgid "runtime.unknown_autoload"
|
||||
msgstr "\"{autoload}\" doesn't appear to be a valid autoload."
|
||||
|
||||
msgid "runtime.something_went_wrong"
|
||||
msgstr "Something went wrong."
|
||||
|
||||
msgid "runtime.expected_n_got_n_args"
|
||||
msgstr "\"{method}\" was called with {received} arguments but it only has {expected}."
|
||||
|
||||
msgid "runtime.unsupported_array_type"
|
||||
msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead."
|
||||
|
||||
msgid "runtime.dialogue_balloon_missing_start_method"
|
||||
msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method."
|
||||
|
||||
msgid "runtime.top_level_states_share_name"
|
||||
msgstr "Multiple top-level states ({states}) share method/property/signal name \"{key}\". Only the first occurance is accessible to dialogue."
|
||||
|
||||
msgid "translation_plugin.character_name"
|
||||
msgstr "Character name"
|
||||
|
||||
msgid "Static ID can't be on a line with no other content."
|
||||
msgstr "Static ID can't be on a line with no other content."
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user