From 99a7068e12bdc9b4feeb94806262b5ea157ac091 Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Tue, 16 Jun 2026 23:51:31 +0200 Subject: [PATCH] Add cross timeline interaction gym --- .agents/skills/dialogic/SKILL.md | 1250 ++++++++++++++++- core/spine_dialogic_portrait.gd | 1 + .../cross-timeline-interactions/README.md | 487 +++++++ .../gym_controller.gd | 108 ++ .../gym_controller.gd.uid | 1 + .../cross-timeline-interactions/gym_main.dtl | 56 + .../gym_main.dtl.uid | 1 + .../gym_pattern1_jump.dtl | 20 + .../gym_pattern1_jump.dtl.uid | 1 + .../gym_pattern1_target.dtl | 33 + .../gym_pattern1_target.dtl.uid | 1 + .../gym_pattern2_caller.dtl | 42 + .../gym_pattern2_caller.dtl.uid | 1 + .../gym_pattern2_subroutine.dtl | 35 + .../gym_pattern2_subroutine.dtl.uid | 1 + .../gym_pattern3_variable_chain_A.dtl | 24 + .../gym_pattern3_variable_chain_A.dtl.uid | 1 + .../gym_pattern3_variable_chain_B.dtl | 30 + .../gym_pattern3_variable_chain_B.dtl.uid | 1 + .../gym_pattern4_signal_chain.dtl | 33 + .../gym_pattern4_signal_chain.dtl.uid | 1 + .../gym_pattern4_signal_target.dtl | 34 + .../gym_pattern4_signal_target.dtl.uid | 1 + .../gym_pattern5_choice_jump.dtl | 37 + .../gym_pattern5_choice_jump.dtl.uid | 1 + .../gym_pattern6_condition_router.dtl | 39 + .../gym_pattern6_condition_router.dtl.uid | 1 + .../gym_pattern7_text_input.dtl | 48 + .../gym_pattern7_text_input.dtl.uid | 1 + .../gym_pattern8_dynamic_label.dtl | 72 + .../gym_pattern8_dynamic_label.dtl.uid | 1 + docs/museums/dialogic/start_timeline.dtl | 20 +- project.godot | 21 + 33 files changed, 2324 insertions(+), 80 deletions(-) create mode 100644 docs/gyms/cross-timeline-interactions/README.md create mode 100644 docs/gyms/cross-timeline-interactions/gym_controller.gd create mode 100644 docs/gyms/cross-timeline-interactions/gym_controller.gd.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_main.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_main.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl.uid create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl create mode 100644 docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl.uid diff --git a/.agents/skills/dialogic/SKILL.md b/.agents/skills/dialogic/SKILL.md index 685ae0a..6204e24 100644 --- a/.agents/skills/dialogic/SKILL.md +++ b/.agents/skills/dialogic/SKILL.md @@ -8,6 +8,7 @@ description: Dialogic 2 addon for Godot 4 — a dialog system with timelines, ev Dialogic is a Godot 4 addon (`addons/dialogic/`) providing a full dialog and visual novel system. It is an autoload (`Dialogic`) with a modular subsystem/event architecture. > **Official docs:** https://docs.dialogic.pro/ +> **Class Reference:** https://docs.dialogic.pro/class_index.html ## Architecture Overview @@ -194,62 +195,106 @@ Events are scripts in `addons/dialogic/Modules//event_.gd`. They e ### Timeline Text Format -Timelines are saved in text format (`.dtl` files). They use TAB indentation to know what events belong to a choice or condition block. Only changes in indentation matter — be consistent within one block. +Timelines use the `.dtl` file extension and are saved in a human-readable text format. You can edit them in any text editor — Dialogic's built-in text editor provides autocompletion and syntax highlighting. -#### Custom-syntax events (bare text, no brackets): +> To find all parameters available on each shortcode event, search for the event class name (e.g. `DialogicBackgroundEvent`) in the Godot Help or the [Dialogic documentation](https://docs.dialogic.pro/). +#### Indentation Rules + +Timelines use **TAB indentation** to determine which events belong to a choice or condition block. Only changes in indentation matter — be consistent within one block, but the exact amount doesn't matter. + +#### Custom-Syntax Events (bare text, no brackets) + +Some events have a special syntax for easier writing: + +**Text Event:** ``` -# Comments (lines starting with #) -# Comment Event — editor-only, ignored at runtime +# Plain text with no speaker — said by no one in particular +A simple line of narration or internal monologue. -# Text Event +# Speaker with optional portrait CharacterName: Dialog text here. -CharacterName (portrait): Dialog text with portrait change. -NoName: Text said by no one in particular. -Long text ending with \ will include the next line too.\ -This line is part of the same text event. +CharacterName (portrait): Dialog text with a portrait change. -# Character Event +# Multi-line text — end a line with \ to include the next line +Emilio: This is a text event with \ +multiple lines. Isn't that great? It is! +``` +The `\` continuation joins the next line into the same text event. Trailing whitespace after `\` is ignored. + +**Character Event:** +``` join CharacterName (portrait) position [animation="Bounce In"] leave CharacterName [animation="Bounce Out" length="0.3"] -update CharacterName (portrait) position [animation="Tada" wait="true"] +update CharacterName (portrait) position [animation="Tada" wait="true" repeat="3" move_time="0.3"] +``` +Positions: `left`, `center`, `right`. The animation shortcode at the end contains optional parameters. -# Condition Event (indentation-based blocks) -if {coins} == 0: - # indented events run when true -elif {health} <= 10: +**Condition Event** (indentation-based blocks): +``` +if {Player.Wisdom} > 3: + # indented events run when condition is true +elif {Player.Health} <= 10: # another branch else: - # fallback - -# Choice Event (each option starts with -) -- Yes, let's go! - # indented events fire when chosen -- No, not now. | [if {relationship} > 23] - # conditional choice -- Maybe | [if {charisma} > 10] [else="disable" alt_text="Maybe [too insecure]"] - -# Set Variable Event -set {MyVariable} += 10 -set {flag} = true - -# Label Event -label LabelIdentifier -label LabelIdentifier (Display Name) - -# Jump Event -jump LabelIdentifier -jump TimelineName/LabelIdentifier # jump to label in another timeline -jump TimelineName/ # jump to beginning of another timeline - -# Return Event -return - -# Do/Call Event -do AutoloadName.method("argument") + # fallback — runs if no condition matched ``` -#### Shortcode events (wrapped in `[...]`): +**Choice Event** (each option starts with `-`): +``` +- Yes, let's go! + # indented events fire when this option is chosen +- No, not now. | [if {John.Relationship} > 23] + # conditional choice — only shown if condition is true +- Maybe | [if {Stats.Charisma} > 10] [else="disable" alt_text="Maybe [too insecure]"] +``` +The `|` separator attaches logic to the choice option. `[if]` controls visibility; `[else]` controls behavior when hidden (`disable` or `hide`). + +**Set Variable Event:** +``` +set {coins} = 20 +set {Player.Health} += 10 +set {Player.Shield} -= 5 +set {Player.Damage} *= 1.5 +set {Player.Speed} /= 2 +set {player_name} = "Miko" +set Autoload.property = "Hello World" +``` +Supported operators: `=`, `+=`, `-=`, `*=`, `/=` + +**Comment Event** (editor-only, ignored at runtime): +``` +# This is a comment — useful for notes and organization +``` + +**Label Event:** +``` +label LabelIdentifier +label LabelIdentifier (Display Name) +``` + +**Jump Event:** +``` +jump LabelIdentifier # jump to a label in the current timeline +jump TimelineName/LabelIdentifier # jump to a label in another timeline +jump TimelineName/ # jump to the beginning of another timeline +``` + +**Return Event:** +``` +return +``` +Returns from a previous `jump` to where the jump originated. + +**Do/Call Event:** +``` +do AutoloadName.method("argument") +``` +Calls a method on any autoload singleton. + +#### Shortcode Events (wrapped in `[...]`) + +Most other events use a shortcode style. Parameters are separated by at least one space (order doesn't matter). All parameter values must be wrapped in double quotes, regardless of type: ``` [background path="res://icon.png" fade="1.0"] @@ -260,9 +305,47 @@ do AutoloadName.method("argument") [clear time=0.5] [style name="MyStyle"] [save slot="auto"] +[text_input text="What's your name?" var="player_name" placeholder="Enter..." default="Hero" allow_empty="false"] ``` -Shortcode parameter values must be in double quotes. Order doesn't matter. +#### Full Example Timeline + +``` +[background path="res://assets/backgrounds/dialogic_factory.png"] + +join Jowan left +Jowan (excited): Hello and welcome to[portrait=confused]...[pause=0.5] Wait? What is this? + +join Emilio (happy) right +Emilio: Well, this is the example timeline. + +Jowan: I thought this was a cool new feature? + +Emilio: Nah, sorry. + +Jowan (sad): It's okay. + +# Choices loop back to this label +label WhatAbout + +Jowan (default): So what should this example be about? +- How to bake cookies + Emilio (confused): Wait that hasn't to do with dialogic?! + jump WhatAbout + +- How to reach the moon | [if {Player.Name} == "NASA"] + Jowan (angry): NASA! It's you again. This is for making dialogs!\ + Please ask someone else about the moon! + + jump WhatAbout + +- How to write timelines in text format + Jowan: Oh, well it's pretty intuitive.[pause=0.2][portrait=questioning] I hope. + + Emilio: Let's hope it's easy as well. + +[end_timeline] +``` ## Characters @@ -290,31 +373,519 @@ Characters are found by identifier string in the `dch_directory`. The directory If a character is not found at runtime, Dialogic creates a temporary character with no portraits. Portraits won't display because `change_speaker()` skips characters with empty portraits. -## Variables +## Character Event -Variables are stored in `ProjectSettings` under `dialogic/variables` as a nested Dictionary. Access/manage via `Dialogic.VAR` subsystem. +The Character event controls how characters with portraits appear, move, and leave the scene. It has three modes: **Join**, **Leave**, and **Update**. -**Important:** Variables must be pre-declared in the Dialogic Variables editor (or `project.godot`) before they can be set by timeline `set` commands. The Variable event reads the current value first and fails if the variable doesn't exist. +### Modes + +| Mode | Keyword | Purpose | +|------|---------|--------| +| **Join** | `join` | Add a character to the scene. Requires a position/transform. Uses `In`-animations. | +| **Leave** | `leave` | Remove a character. Uses `Out`-animations. `--All--` removes all joined characters. | +| **Update** | `update` | Modify an existing character's portrait, position, mirroring, z-index, extra data, or play an animation. Only changes what is explicitly set — unspecified properties stay unchanged. | + +### Text Syntax + +``` +# Basic join — character at a preset position +join MyCharacter center + +# Join with portrait and animation +join MyCharacter (SomePortrait) center [animation="Bounce In"] + +# Join a character whose name contains spaces (quoted) with a grouped portrait +join "My Character" (Special/Crazy) center + +# Join with manual transform (position, size, rotation) +join MyCharacter pos=x0.5y1 size=y400px rot=20 + +# Basic leave +leave MyCharacter + +# Leave with animation parameters +leave MyCharacter [animation="Fade Down"] + +# Leave all characters at once +leave --All-- + +# Update: animate the character +update MyCharacter [animation="Tada" length="2" wait="true"] + +# Update: move character with tween +update MyCharacter left [move_time="1" move_trans="Elastic" move_ease="In"] + +# Update: change portrait with crossfade +update MyCharacter (OtherPortrait) [fade_time="0.5"] +``` + +### Main Settings + +#### Character + +The character identifier (from the `.dch` filename). If the name contains spaces, wrap it in double quotes: `"My Character"`. For `leave`, use `--All--` to remove all joined characters. + +#### Portrait (optional) + +The portrait to display, specified in parentheses after the character: `(portrait_name)`. Portraits in groups use the full path: `(Group/SubGroup/Portrait)`. The portrait can be an expression inside `{curly braces}`: `({PlayerPortrait})`. + +- **Join**: Uses the character's default portrait if omitted. +- **Update**: Does NOT change the portrait unless one is given (gated by `set_portrait`). +- **Leave**: Ignored. + +#### Position / Transform + +Defines where the character appears. Two approaches: + +**Preset positions** — reference a `PortraitContainer` defined in your style layer: +``` +join MyCharacter left +join MyCharacter center +``` +Default presets: `leftmost`, `left`, `center`, `right`, `rightmost`. Add more by creating a custom portrait layer. + +**Manual transform commands** — precise control with `pos=`, `size=`, `rot=`: +``` +join MyCharacter pos=x0.5y1 size=y400px rot=20 +``` +- `pos=x0.5 y1` — position (default: relative to viewport; x=1 is full width, y=1 is full height) +- `pos=x100px y1%` — can also use `px` or `%` units +- `size=x0.3y0.3` — size relative to viewport +- `rot=20` — rotation in degrees around the portrait origin (usually bottom center) + +The position defines the ORIGIN of the portrait container. When joining, position/size/rotation defaults are copied from the first portrait preset found — so `pos=x0.3` alone is usually sufficient to vary along the x-axis. + +### Animation Settings (shortcode) + +All animation parameters go in the `[...]` shortcode at the end: + +| Parameter | Shortcode | Description | +|-----------|-----------|-------------| +| Animation name | `animation="Bounce"` | Animation to play. Forgiving match: `Fade Up`, `fade up in`, `fade In Up` all resolve correctly. Join/Leave fall back to Settings defaults if omitted; Update plays nothing if omitted. | +| Length | `length="1"` | Animation duration in seconds. Default: `0.5`. | +| Wait | `wait="true"` | If true, pauses the timeline until the animation finishes. Default: `false`. | +| Repeat | `repeat="3"` | **Update only.** Number of times to repeat the animation. Default: `1`. | + +### Fade Settings (shortcode, Update mode only) + +Used when changing a portrait during an Update event: + +| Parameter | Shortcode | Description | +|-----------|-----------|-------------| +| Fade animation | `fade="Fade"` | Crossfade animation for portrait transition. Falls back to the default crossfade in Settings if omitted. | +| Fade length | `fade_length="0.5"` | Crossfade duration in seconds. Default: `0.5`. | + +### Move Settings (shortcode, Update mode only) + +Used when changing position during an Update event to tween the movement: + +| Parameter | Shortcode | Description | +|-----------|-----------|-------------| +| Move time | `move_time="0.8"` | Tween duration in seconds. If `0` (default), the change is instant. | +| Move transition | `move_trans="Elastic"` | Tween transition type. | +| Move easing | `move_ease="In Out"` | Tween easing direction. | + +**Transition types:** `Linear`, `Sine`, `Quint`, `Quart`, `Quad`, `Expo`, `Elastic`, `Cubic`, `Circ`, `Bounce`, `Back`, `Spring` + +**Easing types:** `In`, `Out`, `In Out`, `Out In` + +Reference: [Godot Tween Cheat Sheet](https://raw.githubusercontent.com/godotengine/godot-docs/master/img/tween_cheatsheet.webp) + +### Other Settings (shortcode) + +| Parameter | Shortcode | Description | +|-----------|-----------|-------------| +| Z-Index | `z_index="2"` | Controls visual stacking order. Higher values appear in front. NOT Godot's native z_index — Dialogic sorts manually. | +| Mirrored | `mirrored="true"` | Flips the portrait horizontally. Applied on top of the portrait's and container's mirror settings. | +| Extra Data | `extra_data="something"` | Arbitrary string passed to the portrait scene. Handled by `_set_extra_data(data)` on custom portraits. | + +### Portrait Expressions + +The portrait field supports `{curly brace}` expressions evaluated at runtime: +``` +join MyCharacter ({PlayerPortrait}) center +update MyCharacter ({CurrentMood}) +``` +Useful for dynamic portrait switching based on variables. + +### Character with No Portraits + +If a character has no portraits (empty `portraits` dictionary), the Character event editor hides portrait/position options. Timeline text using that character will still parse but no portrait will display. + +## Jump, Label & Return Events + +These three events work together to control timeline flow: **Label** defines anchor points, **Jump** changes the current position (within or across timelines), and **Return** goes back to the last jump origin. Jumps use a **stack** to track where you came from. + +### Text Syntax + +``` +# Jump to the beginning of the current timeline (no target) +jump + +# Jump to a label in the current timeline +jump LabelIdentifier + +# Jump to a label in another timeline +jump TimelineName/LabelIdentifier + +# Jump to the beginning of another timeline +jump TimelineName/ + +# Define a label (anchor point) +label LabelIdentifier + +# Label with a display name (for debugging/history) +label LabelIdentifier (Display Name) + +# Return to the last jump point +return +``` + +**Dynamic labels:** Labels can include `{variables}` for runtime resolution: +``` +jump {day_phase} +``` + +### How the Jump Stack Works + +1. When a **Jump** event executes, the current timeline + event index is **pushed** onto a stack. +2. Execution moves to the target (label, timeline start, etc.). +3. When a **Return** event executes, the stack is **popped** and execution resumes at the saved timeline + index. +4. If the stack is empty when a Return event is hit, the timeline ends (`dialogic.end_timeline()`). + +This allows building reusable timeline "subroutines" — jump to a timeline, run it, and return to exactly where you left off. + +### Subsystem API (`Dialogic.Jump`) ```gdscript -DialogicUtil.get_default_variables() # returns the full variable dict -DialogicUtil.list_variables(dict, path) # flatten to array of paths +# Jump to a specific label in the current timeline (bypasses the stack) +Dialogic.Jump.jump_to_label("LabelIdentifier") + +# Resume from the last jump (pops the stack) +Dialogic.Jump.resume_from_last_jump() + +# Check if the jump stack is empty +if Dialogic.Jump.is_jump_stack_empty(): + print("No jump to return to") + +# Start a timeline at a specific label +Dialogic.start(timeline, "LabelIdentifier") + +# Get info about the last passed label +var label_id = Dialogic.Jump.get_last_label_identifier() +var label_name = Dialogic.Jump.get_last_label_name() +``` + +### Jump Signals + +```gdscript +# Emitted when a Jump event switches to a different timeline +Dialogic.Jump.switched_timeline.connect(func(info): + print("Switched from ", info.previous_timeline, " to ", info.timeline, " at ", info.label)) +# info = {"previous_timeline": DialogicTimeline, "timeline": DialogicTimeline, "label": ""} + +# Emitted when a Jump event jumps to a label (same or different timeline) +Dialogic.Jump.jumped_to_label.connect(func(info): + print("Jumped to label ", info.label, " in ", info.timeline)) +# info = {"timeline": DialogicTimeline, "label": "label_name"} + +# Emitted when a Return event pops the stack +Dialogic.Jump.returned_from_jump.connect(func(info): + print("Returned from ", info.sub_timeline, " to label ", info.label)) +# info = {"sub_timeline": DialogicTimeline, "label": "label_name"} + +# Emitted when a Label event is passed during execution +Dialogic.Jump.passed_label.connect(func(info): + print("Passed label ", info.identifier, " (", info.display_name, ")")) +# info = {"identifier": "name", "display_name": "Name", "display_name_orig": "Name", "timeline": "timeline_id"} +``` + +## Variables + +Dialogic has a built-in variable system with folders, types, and expression evaluation. Variables can also reference external autoload singletons. + +### Storage Architecture + +- **Default values** are stored in `ProjectSettings` under `dialogic/variables` as a nested Dictionary. This is where the Variables editor persists them. +- **Runtime values** are stored in `Dialogic.current_state_info['variables']` — a copy of the defaults merged with current state. +- **Folder grouping** is represented by nested Dictionaries. A dotted path like `Player.Health` maps to `{"Player": {"Health": 100}}`. +- On `Dialogic.VAR.reset()` or `Dialogic.clear(..., false)`, runtime values are reset to the defaults from ProjectSettings. + +### Variable Editor + +Set up variables in the Dialogic "Variables" tab. Each variable has a **name** and **default value**. Variables can be organized into **folders** (nested Dictionaries). Inside a folder, no two items (variable or subfolder) can share the same name. + +**Variable types** (from `DialogicUtil.VarTypes` enum): +- `ANY` — auto-detected from default value +- `STRING` — text +- `FLOAT` — floating point number +- `INT` — integer +- `BOOL` — true/false + +### Referencing Variables + +**Dialogic variables** use dotted paths for folders; wrap in `{curly braces}`: +``` +{coins} # root-level variable +{Player.Health} # variable inside a folder +{FolderA.FolderB.variable_name} # nested folders +``` + +**Autoload variables** are referenced by `AutoloadName.property`: +``` +{MySingleton.player_name} +{GameState.difficulty} +{MyAutoload.get_random_name()} # method calls return their result +``` + +**Complex expressions** are supported: +``` +{{Player.Health}/10.0+2} +``` + +**Important:** Dialogic variables (native, no autoload prefix) **must be pre-declared** in the Variables editor before they can be set by timeline `set` commands or referenced in text. Referencing an undeclared variable evaluates to `null` (empty string in text). Autoload variables do NOT need pre-declaration — they're accessed directly via the `Expression` class. + +**Availability:** Variables are only accessible after the `Dialogic` autoload is `_ready()`. Do not read/write variables in `_ready()` of nodes that load before Dialogic. + +### Using Variables in the Timeline + +#### In Text Events + +Insert variable values with curly braces: +``` +You have {coins} coins remaining. +Hello, {PlayerName}! +Next level at {MyAutoload.xp_required} XP. +``` + +Variables can also be used in: +- **Character Display Names** — type `{MyVariableName}` in the character editor's display name field. +- **Glossary entries** — titles, texts, and extras all support the same `{curly braces}` syntax. + +#### In Conditions + +``` +if {coins} >= 10: + # indented events run when true +elif {Player.Health} <= 5: + # another branch +else: + # fallback +``` + +Conditions can also reference autoloads without curly braces (bare names are treated as autoload names by Godot's `Expression` class): +``` +if GameState.difficulty == "hard": +if MyAutoload.check_info("Argument", false): +``` + +Use typical boolean operators: `==`, `!=`, `<`, `>`, `<=`, `>=`, `not`, `and`, `or`, and parentheses for grouping. + +#### Set Variable Event + +**Text syntax:** +``` +# Dialogic variables (pre-declared) +set {coins} = 20 +set {Player.Health} += 2 +set {Player.Shield} -= 5 +set {Player.Damage} *= 1.5 +set {Player.Speed} /= 2 + +# String assignment +set {player_name} = "Miko" + +# Autoload variables (no pre-declaration needed) +set Autoload.property = "Hello World" +set Autoload.health = {Player.Health} + 2 + +# Complex expressions +set {Player.Health} += range(10, 20).pick_random() +``` + +**Operations:** `=` (SET), `+=` (ADD), `-=` (SUBTRACT), `*=` (MULTIPLY), `/=` (DIVIDE) + +String concatenation is supported with `+=` when both the variable and value are strings. + +**Value types** (selectable in the visual editor): +| Type | Example | Description | +|------|---------|-------------| +| String | `"Hello"` | Quoted string literal | +| Number | `42` or `3.14` | Float or integer | +| Variable | `{OtherVar}` | Value of another variable | +| Bool | `true` / `false` | Boolean literal | +| Expression | `range(10,20).pick_random()` | Complex GDScript expression | +| Random Number | `range(0, 100).pick_random()` | Random integer in range | + +#### Text Input Event + +The Text Input event prompts the player to type text and stores the result in a dialogic variable: +``` +[text_input text="What's your name?" var="player_name" placeholder="Enter name..." default="Hero" allow_empty="false"] +``` +This is a specialized form of set variable that captures player keyboard input. + +### Accessing Variables from Code + +#### Using `Dialogic.VAR` Subsystem + +```gdscript +# Direct property access (simple and preferred) +Dialogic.VAR.coins = 42 +print(Dialogic.VAR.coins) +Dialogic.VAR.Player.Health = 100 # folder access +print(Dialogic.VAR.Player.Health) # 100 + +# String-based access (useful for dynamic variable names) +Dialogic.VAR.set_variable("coins", 42) +print(Dialogic.VAR.get_variable("coins")) +Dialogic.VAR.set_variable("Player.Health", 100) + +# Check existence +if Dialogic.VAR.has("coins"): + print("coins exists") + +# Reset to defaults +Dialogic.VAR.reset() # reset all variables +Dialogic.VAR.reset("Player.Health") # reset one variable to its default + +# Parse curly-brace expressions in strings +var result = Dialogic.VAR.parse_variables("You have {coins} coins.") +``` + +#### Using `DialogicUtil` (static helpers) + +```gdscript +DialogicUtil.get_default_variables() # returns the full default variable dict +DialogicUtil.list_variables(dict, path) # flatten to array of dotted paths DialogicUtil.get_variable_type(path, dict) # get VarTypes enum value - -# VarTypes enum: ANY, STRING, FLOAT, INT, BOOL +DialogicUtil.get_variable_value_type(value) # get type of a value +DialogicUtil.logical_convert(value) # auto-convert string to int/float/bool ``` -### Variable references in expressions +#### Variable Folders -Dialogic variables are referenced with **curly braces** in expressions (conditions, set values, choice conditions): +When accessing via direct property access (`Dialogic.VAR.MyFolder`), folders return a `VariableFolder` object that supports: +- `folders()` — list all subfolders (as `VariableFolder` objects) +- `variables(absolute=false)` — list variable names in this folder +- `has(name)` — check if a variable or subfolder exists +- Direct property access to child variables: `Dialogic.VAR.Towns.TownA.Church` -``` -if {coins} == 0: -set {coins} += 10 -- Option | [if {player_has_key}] +Example: +```gdscript +# Pick a random location from nested folder structure +var location = Dialogic.VAR.Towns.folders().pick_random().variables().pick_random() ``` -Bare names like `coins` are NOT resolved as variables — they're treated as autoload properties by Godot's `Expression` class. +### Variable Signals + +```gdscript +# Emitted whenever any Dialogic variable changes +Dialogic.VAR.variable_changed.connect(func(info): + print(info.variable, " changed to ", info.new_value)) +# info = {"variable": "variable_name", "new_value": new_value} + +# Emitted specifically from Set Variable events (not direct code sets) +Dialogic.VAR.variable_was_set.connect(func(info): + print(info.variable, ": ", info.orig_value, " -> ", info.new_value)) +# info = {"variable": name, "orig_value": old, "new_value": result, "value": change_value, "value_str": raw_string} +``` + +## Writing Text + +When writing text in Text events or Choice events, you can use four complementary systems: **BBCode**, **Variables/Expressions**, **Text Modifiers**, and **Text Effects**. + +### BBCode (Styling) + +Standard Godot BBCode tags work in Text events for visual styling. Full reference: [BBCode in RichTextLabel](https://docs.godotengine.org/en/stable/tutorials/ui/bbcode_in_richtextlabel.html). + +Common tags: +``` +[b]Bold[/b] [i]Italic[/i] [u]Underline[/u] [s]Strikethrough[/s] +[color=#ff0000]Colored text[/color] [color=red]Named color[/color] +[font=res://my_font.ttf]Custom font[/font] [font_size=24]Bigger text[/font_size] +[img=32x32]res://icon.png[/img] # inline image +[url=https://example.com]Link[/url] # clickable link +[wave amp=20 freq=2]Wave effect[/wave] # animated wave +[tornado radius=10 freq=5]Tornado effect[/tornado] # animated shake +[shake rate=10 level=15]Shake effect[/shake] # animated shake +[fade start=4 length=20]Fade effect[/fade] # fading characters +[rainbow freq=0.2 sat=1 val=1]Rainbow text[/rainbow] # color cycling +``` + +**Important:** BBCode tags must be properly paired with closing tags (`[/tag]`). This differs from Text Effects which have no closing tag. + +**Choice buttons:** Standard choice buttons use `Label` (not `RichTextLabel`), so BBCode won't work there. To get BBCode on choices, use a custom choice button with `RichTextLabel`. + +### Variables / Expressions in Text + +Anything in `{curly braces}` inside text is treated as an expression and evaluated at display time. The result is inserted as text: + +``` +You have {coins} coins remaining. +Hello, {PlayerName}! +Current HP: {Player.Health} # folder path +Next level at {MyAutoload.xp_required} XP. # autoload property +Your lucky number is {MySingleton.get_random_name()} # autoload method +Stat bonus: {{Player.Health}/10.0+2} # complex expression +``` + +- **Dialogic variables** — resolved by dotted path: `{coins}`, `{Player.Health}`, `{FolderA.FolderB.var}` +- **Autoload properties** — resolved via Godot's `Expression` class: `{MySingleton.some_property}` +- **Autoload methods** — called and result inserted: `{MyAutoload.get_random_name()}` +- **Complex expressions** — nested braces for calculations: `{{Player.Health}/10.0+2}` + +**Important:** Dialogic-native variables must be pre-declared in the Variables editor. Referencing an undeclared variable evaluates to `null` and displays as an empty string. Autoload variables don't need pre-declaration. + +### Text Modifiers (Preprocessors) + +Text modifiers run **before** the text is displayed, transforming the text string. They work in both Text events and Choice events (most of them). + +See the [Text Modifiers](#text-modifiers-preprocessors) section below for the full list. + +### Text Effects (Reveal-Time Actions) + +Text effects are tags that trigger an action when the text reveal reaches that position. They are NOT paired — there is no closing tag. They work only in Text events. + +See the [Text Effects](#text-effects-inline-bbcode) section below for the full list. + +### Text Speed System + +Dialogic has three interacting speed controls: + +| Control | Scope | Description | +|---------|-------|-------------| +| **Letter speed** | Per-event (via `[lspeed]`) | Time in seconds for each letter to reveal. Default set in Text Settings. | +| **Speed multiplier** | Per-event (via `[speed]`) | Multiplies letter speed and pause length. 1 = normal, 0 = instant, >1 = slower. Resets each Text event. | +| **Text Speed setting** | Global (via `Dialogic.Settings`) | Global multiplier; stored automatically. Use for in-game speed options: `Dialogic.Settings.text_speed = 0.5` | + +All three multiply together: `actual_delay = letter_speed × speed_multiplier × text_speed_setting` + +Adding `!` to `[lspeed=x!]` or `[pause=x!]` makes the value absolute (ignores both multipliers). + +### Custom Text Modifiers + +Custom modules can register text modifiers by implementing `_get_text_modifiers()`: + +```gdscript +func _get_text_modifiers() -> Array[Dictionary]: + return [ + { + "subsystem": "MySubsystem", + # or "node_path": "/root/MyAutoload", + "method": "my_modifier_method", + "mode": -1 # -1 = Text & Choice, 0 = Texts only, 1 = Choices only + } + ] +``` + +The modifier method receives a `String` and must return a `String`: + +```gdscript +func my_modifier_method(text: String) -> String: + # transform text here (regex, custom syntax, etc.) + return text +``` ## Text Effects (inline BBCode) @@ -323,7 +894,7 @@ Text effects are tags within TextEvent text that trigger actions when reached du | Effect | Syntax | Purpose | |--------|--------|---------| | Speed | `[speed=x]` / `[speed]` | Set speed multiplier (1 = normal, 0 = instant). Resets per event. | -| Letter Speed | `[lspeed=x]` / `[lspeed=x!]` | Set letter reveal time in seconds. `!` makes it absolute (ignores multiplier). | +| Letter Speed | `[lspeed=x]` / `[lspeed]` / `[lspeed=x!]` | Set letter reveal time in seconds. Bare resets to default. `!` makes it absolute (ignores multipliers). | | Pause | `[pause=x]` / `[pause=x!]` | Pause reveal for x seconds. `!` makes it absolute. | | Portrait | `[portrait=name]` | Switch speaker's portrait mid-text. | | Signal | `[signal=arg]` | Emit `Dialogic.text_signal` with argument. | @@ -347,21 +918,387 @@ Text modifiers run on text before display: | Conditional Text | `[if {cond} Text if true./Text if false.]` | Inline conditional text | | Autopauses | (automatic) | Insert `[pause]` after configured punctuation characters | -## Styles & Layouts +## Auto-Advance -Styles define the visual presentation. Layouts define the scene structure. +Auto-Advance automatically progresses the timeline without user input — ideal for hands-free reading or visual novel "auto-forward" modes. It is managed through `Dialogic.Inputs.auto_advance`, a `DialogicAutoAdvance` object. -- **Dialogic Nodes**: UI nodes (DialogText, NameLabel, PortraitContainer, ChoiceButton, etc.) that display things. They register themselves in groups. -- **Layout Scenes**: Scenes containing Dialogic Nodes and custom scripts. Base layout extends `DialogicLayoutBase`, layers extend `DialogicLayoutLayer`. -- **Styles**: Resources combining one base layout + list of layer layouts with overridable settings. Can inherit from other styles. +### Enable Flags (Stackable) + +Auto-Advance uses three independent boolean flags — if **any** is `true`, Auto-Advance is active. This stacking allows multiple systems to enable it without conflicting: + +| Flag | Set via | Stays enabled until | +|------|---------|---------------------| +| `enabled_until_user_input` | Player toggles Auto-Advance on | Player presses any Dialogic input action | +| `enabled_until_next_event` | `[aa]` text effect or code | The next text event begins | +| `enabled_forced` | Code: `auto_advance.enabled_forced = true` | Code: `auto_advance.enabled_forced = false` | + +**Example:** If the player enables Auto-Advance AND a text event uses `[aa]`, disabling one flag won't stop it until all flags are cleared. + +### Configuring from Code ```gdscript -Dialogic.Styles.load_style("StyleName") # load/switch to a style -Dialogic.Styles.has_active_layout_node() # check if layout is present -Dialogic.Styles.get_layout_node() # get the layout scene node -Dialogic.Styles.change_style("StyleName") # change style, preserving state +# Enable Auto-Advance (player toggle) +Dialogic.Inputs.auto_advance.enabled_until_user_input = true + +# Enable until next event (e.g., for a cutscene) +Dialogic.Inputs.auto_advance.enabled_until_next_event = true + +# Force enable (script-controlled) +Dialogic.Inputs.auto_advance.enabled_forced = true + +# Check if currently enabled +if Dialogic.Inputs.auto_advance.is_enabled(): + print("Auto-advancing") + +# Check if currently waiting (timer active) +if Dialogic.Inputs.auto_advance.is_advancing(): + print("Timer running: ", Dialogic.Inputs.auto_advance.get_time_left()) ``` +### Delay Calculation + +When Auto-Advance starts, the delay is calculated from the current text: + +``` +delay = (per_word_delay × word_count + per_character_delay × char_count) × delay_modifier + fixed_delay +``` + +| Property | Default | Description | +|----------|---------|-------------| +| `fixed_delay` | `1.0` | Base delay in seconds added after other calculations | +| `per_word_delay` | `0.0` | Extra seconds per whitespace-separated word | +| `per_character_delay` | `0.1` | Extra seconds per character (excluding ignored characters) | +| `delay_modifier` | `1.0` | Global multiplier on per-word + per-character delays (NOT on fixed_delay). Set via `Dialogic.Settings.autoadvance_delay_modifier` for player-configurable reading speed. | +| `await_playing_voice` | `true` | If true, waits for voice audio to finish before advancing | +| `ignored_characters_enabled` | `false` | If true, certain characters are excluded from the character count | +| `ignored_characters` | `{}` | Dictionary of characters to ignore when counting | + +**Temporary override** per event via `[aa=x]`: sets `override_delay_for_current_event`, which replaces the entire calculation (but still respects `await_playing_voice`). + +```gdscript +# Adjust delays dynamically (e.g., for different languages) +Dialogic.Inputs.auto_advance.per_word_delay = 0.3 +Dialogic.Inputs.auto_advance.per_character_delay = 0.1 + +# Set a global reading speed modifier (player setting) +Dialogic.Settings.autoadvance_delay_modifier = 0.5 # 2x faster +``` + +### Text Effect: `[aa]` + +| Syntax | Behavior | +|--------|----------| +| `[aa]` | Enables Auto-Advance until the next text event | +| `[aa=2]` | Enables Auto-Advance AND sets the delay to 2 seconds (absolute, ignores per_word/per_character) | +| `[aa=2?]` | Does NOT enable Auto-Advance, but sets the delay to 2 seconds IF already enabled | + +### Signals + +```gdscript +# Emitted when Auto-Advance should progress (triggers handle_next_event) +Dialogic.Inputs.auto_advance.autoadvance + +# Emitted when Auto-Advance toggles on/off +Dialogic.Inputs.auto_advance.toggled.connect(func(enabled: bool): + print("Auto-Advance ", "enabled" if enabled else "disabled")) + +# Force an immediate advance +Dialogic.Inputs.auto_advance.autoadvance.emit() +``` + +### Querying Progress + +```gdscript +# Timer progress from 0.0 to 1.0 (returns -1 if not advancing) +var progress = Dialogic.Inputs.auto_advance.get_progress() + +# Time remaining before the next advance +var remaining = Dialogic.Inputs.auto_advance.get_time_left() +``` + +Useful for building an Auto-Advance indicator UI (progress bar or countdown) in your layout layer. + +## Manual-Advance + +Manual-Advance is the default way players progress through timelines — clicking or pressing the `dialogic_default_action` input to move forward event by event. It's managed through `Dialogic.Inputs.manual_advance`, a `DialogicManualAdvance` object. + +### Controlling Manual-Advance + +Two properties control whether the player can advance: + +| Property | Type | Description | +|----------|------|-------------| +| `system_enabled` | `bool` (default `true`) | Master switch. When `false`, player input is completely ignored. | +| `disabled_until_next_event` | `bool` (default `false`) | Temporarily blocks input for the current event only. Overrides `system_enabled`. | + +```gdscript +# Disable all player input (e.g., during a cutscene) +Dialogic.Inputs.manual_advance.system_enabled = false + +# Block input just until the next event (e.g., [ns] text effect uses this) +Dialogic.Inputs.manual_advance.disabled_until_next_event = true + +# Check if the player can currently advance +if Dialogic.Inputs.manual_advance.is_enabled(): + print("Player can advance") +# Equivalent to: system_enabled and not disabled_until_next_event +``` + +### Input Events + +Dialogic listens for the input action configured in `dialogic/text/input_action` (default: `dialogic_default_action`). Input is handled via: +- **Keyboard/controller** → `_unhandled_input()` (non-mouse events) +- **Mouse** → `_input()` or `handle_node_gui_input()` (via InputCatcher/Textbox nodes) + +**Blocking input temporarily:** Dialogic automatically blocks input for ~100ms after text finishes revealing to prevent accidental double-clicks. Call `Dialogic.Inputs.block_input(time)` to extend this. + +```gdscript +# Block all input for 0.5 seconds +Dialogic.Inputs.block_input(0.5) + +# Check if input is currently blocked +if Dialogic.Inputs.is_input_blocked(): + print("Input is blocked") +``` + +### Input Action Signals + +```gdscript +# Priority signal — fires first. Set action_was_consumed = true to swallow the input +Dialogic.Inputs.dialogic_action_priority.connect(func(): + if some_condition: + Dialogic.Inputs.action_was_consumed = true) + +# Main action signal — fires if action_was_consumed is still false +Dialogic.Inputs.dialogic_action.connect(_on_dialogic_action) + +# Check if the input came from a mouse +if Dialogic.Inputs.input_was_mouse_input: + print("Mouse click") +``` + +## Auto-Skip + +Auto-Skip rapidly advances through the timeline, bypassing animations, text reveal, and audio. It's useful for quickly navigating already-seen story branches or debugging. Managed through `Dialogic.Inputs.auto_skip`, a `DialogicAutoSkip` object. + +### Core Settings + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `enabled` | `bool` | `false` | Master toggle. Toggling it emits the `toggled` signal. | +| `time_per_event` | `float` | `0.1` | Maximum seconds each event may take. Events themselves must respect this — it's not enforced centrally. | +| `skip_voice` | `bool` | `true` | If true, voice audio is skipped instead of played. | + +### Auto Disable/Enable Conditions + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `disable_on_user_input` | `bool` | `true` | Disables Auto-Skip when the player presses any Dialogic input action. | +| `disable_on_unread_text` | `bool` | `false` | Disables when reaching a text event the player hasn't seen before. Requires History subsystem. | +| `enable_on_visited` | `bool` | `false` | Enables Auto-Skip when reaching a previously-visited text event. | + +```gdscript +# Toggle auto-skip (e.g., from a button) +Dialogic.Inputs.auto_skip.enabled = !Dialogic.Inputs.auto_skip.enabled + +# Debug mode: skip everything including unseen text +Dialogic.Inputs.auto_skip.disable_on_unread_text = false +Dialogic.Inputs.auto_skip.enabled = true + +# Reduce time per event for faster skipping +Dialogic.Inputs.auto_skip.time_per_event = 0.05 +``` + +### Auto-Skip Signal + +```gdscript +Dialogic.Inputs.auto_skip.toggled.connect(func(is_enabled: bool): + if is_enabled: + print("Auto-Skip started") + else: + print("Auto-Skip stopped")) +``` + +### Implementing Auto-Skip in Custom Events + +Auto-Skip behavior must be implemented per event. Two patterns: + +**Capping time-based operations:** +```gdscript +var animation_length: float = 10.0 + +if Dialogic.Inputs.auto_skip.enabled: + animation_length = min(Dialogic.Inputs.auto_skip.time_per_event, animation_length) +``` + +**Reacting to the toggle for signal-based events:** +```gdscript +func _execute() -> void: + if Dialogic.Inputs.auto_skip.enabled: + finish() # skip instantly + return + # Normal execution path + await some_signal + finish() +``` + +### Summary: Three Ways to Advance + +| Method | Trigger | Speed | Player Control | +|--------|---------|-------|----------------| +| **Manual-Advance** | Player input (`dialogic_default_action`) | Player-paced | Full | +| **Auto-Advance** | Timer after text reveal | Configurable delay (per-word/character) | Player toggles on/off | +| **Auto-Skip** | Timeline events auto-execute | Time-capped per event | Player toggles on/off; stops on unseen text | + +## Styles & Layouts + +Three systems work together to display timelines: **Dialogic Nodes**, **Layout Scenes**, and **Styles**. + +### Concepts + +- **Dialogic Nodes** (`DialogicNode_*`) — UI control nodes (DialogText, NameLabel, PortraitContainer, ChoiceButton, etc.) that are activated by Dialogic subsystems. They register themselves in groups (`dialogic_set_DialogText`, `dialogic_set_NameLabel`, etc.) so subsystems can find them. +- **Layout Scenes** — Reusable scenes containing Dialogic Nodes, other Godot Control nodes, and custom scripts. They provide self-contained UI parts (e.g. `VisualNovel Textbox Scene`, `Glossary Popup Scene`, `Centered Choices Scene`). + - **Base layout** extends `DialogicLayoutBase` — the root scene that holds everything. + - **Layer layouts** extend `DialogicLayoutLayer` — child scenes added as layers to the base. +- **Styles** (`.tres` resources of type `DialogicStyle`) — combine one base layout + list of layer layouts with overridable export settings. Styles can **inherit** from other styles (inherits scenes and default settings). + +### Style Structure + +A `DialogicStyle` resource contains: +- `name` — style identifier (used by `[style]` events, `load_style()`) +- `inherits` — optional parent style for inheritance +- `layer_list` — ordered array of layer IDs +- `layer_info` — Dictionary mapping layer ID (empty string `""` for the base) to `DialogicStyleLayer` resources, each holding: + - `scene` — `PackedScene` reference + - `overrides` — Dictionary of export variable overrides applied at runtime + +### Style Inheritance + +A style can inherit from another. The inheritance chain works as follows: +- `get_layer_inherited_list()` returns the **root ancestor's** layer list (layers are always defined by the root). +- `get_layer_inherited_info(id)` merges overrides walking **up** the chain — child overrides override ancestor overrides. +- `get_inheritance_root()` walks the chain to the topmost style. +- `realize_inheritance()` flattens inheritance into the local style, then detaches the parent. + +### Style Editor (Dialogic UI) + +In the Dialogic Style Editor tab you can: +- Add new styles (empty or from a premade template) +- Rename styles +- Add, replace, or remove layers (both premade and custom scenes) +- Override export settings on any scene in the style +- Make a premade layer or whole style **custom** (copies the scene into your project so you can edit it) +- Create inherited styles +- Change the default style + +**Important:** When customizing a premade layout, make sub-resources (fonts, images, scripts) **unique** before modifying them. Resources still inside `addons/dialogic/` may be lost when updating Dialogic. Using "Make Custom" on the layer automatically handles the root script. + +### Subsystem API (`Dialogic.Styles`) + +```gdscript +# Load/switch styles +Dialogic.Styles.load_style("StyleName") # load a fresh style (use before Dialogic.start() or select style before _ready) +Dialogic.Styles.change_style("StyleName") # switch style mid-dialog, preserving current state (reloads dialog nodes) +Dialogic.Styles.preload_style("StyleName") # preload scene resources in background + +# Query current state +Dialogic.Styles.has_active_layout_node() # bool: is a layout node present in the tree? +Dialogic.Styles.get_layout_node() # returns the DialogicLayoutBase root node (null if none) +Dialogic.Styles.get_current_style() # returns the current style name (empty string if none) + +# Node lookup within the active layout +Dialogic.Styles.get_first_node_in_layout("group_name") # like get_first_node_in_group() but scoped to the layout subtree + +# Signals +Dialogic.Styles.style_changed.connect(_on_style_changed) # emits info: {"style": name, "previous": prev_name} +``` + +`load_style()` vs `change_style()`: +- `load_style()` — use when no layout is active yet (e.g. before `Dialogic.start()`). Creates a fresh layout. +- `change_style()` — use mid-dialog. It calls `load_style()` with `state_reload=true`, which reloads the current dialog state into the new layout's nodes after the new scene is ready. This preserves text, portraits, choices, etc. across the style switch. + +When `load_style()` detects the same scene setup (same inheritance root), it only applies new export overrides without recreating nodes — this is fast and non-disruptive. + +### Changing Styles at Runtime + +**Via timeline event:** +```gdscript +[style name="MyStyle"] +``` +This calls `Dialogic.Styles.change_style()` and waits one frame for the layout to be ready before continuing. + +**Via code (pre-start):** +```gdscript +Dialogic.Styles.load_style("MyStyle") +Dialogic.start("my_timeline") +``` + +**Per-character style:** +Assign a style to a `DialogicCharacter` resource (in the Character editor). When that character speaks, Dialogic temporarily switches to their style (non-base style), then reverts when another character speaks or the base style is restored. + +### Creating Custom Layout Scenes + +To create a custom layout scene from scratch: + +1. Create a new scene with a root node extending `DialogicLayoutBase` (for a base) or `DialogicLayoutLayer` (for a layer). +2. Export variables on the root script — they automatically appear as settings in the Style Editor. Use `@export_group` and `@export_subgroup` for organization. Not all variable types are supported. +3. Override `_apply_export_overrides()` to apply exported settings to your nodes. This method is called: + - When the style is first loaded + - When switching to a different style that uses the same scene (only overrides are re-applied) + +**Base layout** (`DialogicLayoutBase`) additionally provides: +- `add_layer(layer)` — add a `DialogicLayoutLayer` as a child +- `get_layers()` — get all layer children +- `get_global_setting(setting, default)` — retrieve a setting that layers can share (e.g. color themes) +- `_get_persistent_info()` / `_load_persistent_info(info)` — persist and restore data across style changes via `Engine` metadata + +**Layer layout** (`DialogicLayoutLayer`) additionally provides: +- `disabled` export — if `true`, the layer is hidden and processing disabled +- `apply_overrides_on_ready` export — if `true`, `_apply_export_overrides()` runs on `_ready()` (automatically set when making a layer custom) +- `get_global_setting(setting, default)` — delegates to the parent base + +**Adding custom scenes to a style:** +Use the Style Editor's **Add Layer** or **Replace Layer** buttons above the layer list. Your custom scene appears as an option. + +### Creating Custom Dialogic Nodes + +Each Dialogic Node is a normal Control node with a script that registers itself in a group. Subsystems find their nodes via groups. Customizing a Dialogic node means: +1. Duplicate the existing node scene and modify it. +2. Ensure it still registers itself in the expected group(s). +3. In most cases, reacting to Dialogic signals (or signals of existing nodes) is easier than creating a custom node from scratch. + +### Preloading for Performance + +To avoid lag when a style is first loaded, preload it during a loading screen: +```gdscript +# Request threaded preload +Dialogic.Styles.preload_style("MyStyle") + +# Or call on the style resource directly +style_resource.prepare() +``` + +### Project Settings + +Key settings under `dialogic/layout/`: +- `default_style` — path to the default style resource +- `end_behaviour` — 0=free (keep layout), 1=hide (hide on end), 2=keep layout visible +- `style_list` — array of all style resource paths +- `style_directory` — dictionary mapping style names → paths (built automatically) + +### StyleEvent Timeline Shortcode + +```gdscript +[style name="StyleName"] +``` + +### Key Troubleshooting + +- **Style not found at runtime:** Ensure the style `.tres` file is listed in `dialogic/layout/style_list` in ProjectSettings. The `style_directory` is rebuilt by `DialogicStylesUtil.update_style_directory()`. +- **Changes lost on update:** After using "Make Custom", ensure sub-resources (fonts, images, shaders) are saved outside `addons/dialogic/`. Click "Make Unique" on any shared sub-resources. +- **Layout not appearing:** `Dialogic.start()` calls `load_style()` automatically if no layout is active. `Dialogic.start_timeline()` does NOT — you must call `Dialogic.Styles.load_style()` first. +- **Double layout nodes:** Guard with `if not Dialogic.Styles.has_active_layout_node():` before calling `load_style()`. + ## Signals & Signal Events ### Signal Event @@ -398,6 +1335,9 @@ Dialogic.Text.about_to_show_text(info: Dictionary) Dialogic.Text.text_finished(info: Dictionary) Dialogic.Text.speaker_updated(character: DialogicCharacter) Dialogic.Text.textbox_visibility_changed(visible: bool) +Dialogic.Text.animation_textbox_new_text # emitted when new text starts animating in +Dialogic.Text.animation_textbox_show # emitted when textbox show animation starts +Dialogic.Text.animation_textbox_hide # emitted when textbox hide animation starts Dialogic.Portraits.character_joined(info: Dictionary) Dialogic.Portraits.character_left(info: Dictionary) Dialogic.Portraits.character_portrait_changed(info: Dictionary) @@ -426,6 +1366,168 @@ if Dialogic.Save.has_slot("slot_name"): Savegames store Dialogic's timeline state and variables. Extra info is stored per-slot but not loaded into Dialogic state. +### Global Data (Slot-Independent) + +For data that should persist across all save slots (e.g., settings, preferences), use `Dialogic.Settings` or `Dialogic.Save` global info: + +```gdscript +# Simple: store directly on Settings (auto-saved) +Dialogic.Settings.text_speed = 0.5 + +# Explicit: store arbitrary global key-value pairs +Dialogic.Save.set_global_info("player_name", "Miko") +var name = Dialogic.Save.get_global_info("player_name", "Unknown") +``` + +### Saving Visited Events History + +Dialogic's History subsystem can persist which text events the player has already seen: + +```gdscript +# Enable auto-saving visited history on every save +Dialogic.History.save_already_visited_history_on_save = true +Dialogic.History.save_already_visited_history_on_autosave = true + +# Manual control +Dialogic.History.save_visited_history() +Dialogic.History.load_visited_history() +Dialogic.History.reset_already_visited_history() +``` + +### Slot Management + +```gdscript +# Check if a slot exists +if Dialogic.Save.has_slot("slot_name"): + Dialogic.Save.load("slot_name") + +# Delete a slot +Dialogic.Save.delete_slot("slot_name") + +# Create an empty slot +Dialogic.Save.add_empty_slot("slot_name") +``` + +### Manual Save/Load (Full State) + +For custom save pipelines, capture and restore Dialogic's entire state: + +```gdscript +var state: Dictionary = Dialogic.get_full_state() +# ... store/retrieve state however you want ... +Dialogic.load_full_state(state) +``` + +## Reference Manager + +Dialogic's **Reference Manager** (toolbar Link button) handles unique identifiers for timelines and characters, and fixes broken references after renames. + +### Unique Identifiers + +Each `.dtl` timeline and `.dch` character gets a **unique identifier** string (initially derived from the filename, but editable). No two resources of the same type can share an identifier. These identifiers are what you use in timeline text (`CharacterName:`, `jump TimelineName/`). + +In the Reference Manager's second tab, you can double-click any identifier to rename it. Renaming automatically adds an entry to the "Broken References" tab. + +### Broken References + +When you rename a resource, timelines referencing the old identifier break. The Reference Manager's first tab helps fix this: + +1. Dialogic may auto-add renames. Review them. +2. Click **Check Selected** to scan all timelines for the references. +3. Review the results, uncheck any false positives. +4. Click **Replace** to apply. + +> **Warning:** This is a destructive bulk operation on all affected timelines. Commit to version control first. + +### Custom Replacements (Advanced) + +Click the **Plus button** for manual replacements: +- **Pure Text** — custom string or regex. Use `(?something)` group to replace only a portion. +- **Variable / Portrait / Character / Timeline** — specialized regex for each type. +- Optionally **limit to a specific character** (useful for portrait renames). + +## Glossary + +Dialogic's built-in glossary auto-highlights defined terms in dialog text and shows a tooltip with the definition on hover. + +### How It Works + +A glossary is a `.tres` resource with an `entries` array of Dictionaries. Each entry has: +- `name` — the primary term +- `alternatives` — aliases that also trigger highlighting +- `text` — definition shown in the tooltip +- `extra` — additional data + +When a glossary is loaded, Dialogic scans text for matching names/alternatives and applies highlighting automatically. Variables can be used in glossary titles, texts, and extras via `{curly braces}`. + +### Accessing Glossaries from Code + +```gdscript +# Get all glossary file paths +var paths: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', []) + +# Load and inspect a glossary +var glossary: DialogicGlossary = load(paths[0]) +for entry in glossary.entries: + print(entry.name, ": ", entry.text) +``` + +**Note:** Properties prefixed with `_` (like `_translation_id`, `_translation_keys`) are private — use the helper methods on `DialogicGlossary` instead. + +### Translation + +Glossary entries are translatable. Enable translation in Dialogic Settings, then use "Update CSV files". Entry names and alternatives must remain unique across translations, and may not start with `Glossary/`. + +## Translation + +Dialogic supports translating timelines, character names, and glossary entries via CSV files. It uses Godot's built-in `TranslationServer`. + +### Setup + +1. In Dialogic **Settings → Translation**, enable translation. +2. Choose a **default locale** (the language you write in — used as fallback). +3. Set **translation file mode**: "Per Project" (one CSV) or "Per Timeline" (one CSV per `.dtl`). +4. Set a **translation folder** to keep CSV files organized. + +### Workflow + +``` +1. Write/Edit timeline → 2. "Update CSV files" → 3. Edit CSV translations + ↑ ↓ + └────────────────── 4. "Collect translation" ←──────────────┘ +``` + +**CSV format:** +```csv +keys,en,ja +Text/1/text,Hello World!,こんにちは世界! +``` +- Column headers are locale codes (`en`, `ja`, `fr`, etc.). +- Each row's key identifies the translatable unit: `Text//text`, `Character//name`, `Glossary//name`, etc. +- Commas in text must be escaped by wrapping in quotes. + +**Important:** After editing a translated text's original, you MUST regenerate by clicking "Update CSV files" again, or translations won't apply to the changed text. + +### Translation IDs in Timelines + +After enabling translation, Dialogic inserts `#id:` markers at the end of text events: +``` +Character: Hello world! #id:14 +- Yes, I do! #id:16 +``` +These IDs link the text to its CSV row. They are not visible during gameplay. + +### Changing Language + +```gdscript +# Switch to Japanese at runtime +TranslationServer.set_locale("ja") +``` + +### Testing in Editor + +Use the "Testing locale" dropdown in Settings → Translation. This is editor-only and may not work in exported builds. + ## Audio Channels Audio plays on named channels. Default channels defined in project settings: @@ -523,8 +1625,16 @@ func _on_dialog_finished(): **Setting variables from code:** ```gdscript +# Direct property access (preferred) +Dialogic.VAR.player_name = "Miko" +Dialogic.VAR.coins = 42 + +# String-based access for dynamic names Dialogic.VAR.set_variable("player_name", "Miko") Dialogic.VAR.set_variable("coins", 42) + +# Reset all variables to defaults +Dialogic.VAR.reset() ``` **Creating timelines in code:** @@ -533,9 +1643,9 @@ Dialogic.VAR.set_variable("coins", 42) var events: Array = """ Jowan (Surprised): Wow this is interesting! - Yes - set {MyAutoload.excitement} += 20 + set {excitement} += 20 - No - set {MyAutoload.excitement} -= 10 + set {excitement} -= 10 """.split('\n') var timeline = DialogicTimeline.new() timeline.events = events diff --git a/core/spine_dialogic_portrait.gd b/core/spine_dialogic_portrait.gd index 8bb6ee7..9ccbb39 100644 --- a/core/spine_dialogic_portrait.gd +++ b/core/spine_dialogic_portrait.gd @@ -11,6 +11,7 @@ extends DialogicPortrait func _ready() -> void: $SpineSprite.animation_completed.connect(_on_animation_completed) +# Just a signal connection example func _on_animation_completed(spine_sprite: Object, animation_state: SpineAnimationState, track_entry: SpineTrackEntry) -> void: print(track_entry) pass diff --git a/docs/gyms/cross-timeline-interactions/README.md b/docs/gyms/cross-timeline-interactions/README.md new file mode 100644 index 0000000..3dfbe01 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/README.md @@ -0,0 +1,487 @@ +# Cross-Timeline Interactions Gym + +## Overview + +Dialogic 2 provides **seven distinct mechanisms** for multiple timelines to interact +with each other. This gym catalogs and demonstrates every mechanism, so you can choose +the right one for your narrative structure. + +--- + +## The Seven Mechanisms + +| # | Mechanism | Timeline Syntax | GDScript Required? | Preserves Jump Stack? | +|---|-----------|-----------------|-------------------|----------------------| +| 1 | **Jump to another timeline** | `jump TimelineName/Label` | No | Yes (push) | +| 2 | **Jump + Return (subroutine)** | `jump` → … → `return` | No | Yes (push/pop) | +| 3 | **Choice → different timeline** | `- Option` → `jump TimelineB/` | No | Depends | +| 4 | **Condition → different timeline** | `if {var}:` → `jump TimelineB/` | No | Depends | +| 5 | **Variable set → condition in next timeline** | `set {x} = 1` → `if {x}:` in other timeline | No (but needs trigger) | N/A | +| 6 | **Signal event → code → `start_timeline()`** | `[signal arg=...]` | **Yes** | No (manual) | +| 7 | **`do` Call event → autoload method → `start_timeline()`** | `do MyAutoload.method()` | **Yes** | No (manual) | +| 8 | **GDScript `timeline_ended` signal → chain** | None | **Yes** (external) | N/A | +| 9 | **Text input → variable → subsequent timeline** | `[text_input ...]` → `if {name}:` | No | N/A | +| 10 | **Dynamic label jump (`jump {variable}`)** | `jump {day_phase}` | No | Yes | + +--- + +## Mechanism 1: Direct Jump Between Timelines + +The simplest form of cross-timeline interaction. A `jump` event in one timeline immediately +transitions to another timeline (at a specific label or the beginning). + +### Timeline Text + +``` +# timeline_a.dtl +join Miko center +Miko (neutral): I need to go somewhere else now. +jump timeline_b/arrival_label + +# timeline_b.dtl +label arrival_label +join Advisor left +Advisor (pl5): Welcome! You came from timeline_a. +``` + +### What Happens +- The current timeline + event index is **pushed** onto the jump stack +- Dialogic loads `timeline_b` and starts at `arrival_label` (or the beginning if no label) +- `Dialogic.Jump.switched_timeline` and `Dialogic.Jump.jumped_to_label` signals fire +- The old timeline is NOT destroyed — it lives on the jump stack + +### Key Property +**Jump to label in another timeline:** `jump TimelineName/LabelIdentifier` +**Jump to beginning of another timeline:** `jump TimelineName/` + +--- + +## Mechanism 2: Jump + Return (Subroutine Pattern) + +A timeline acts as a reusable "subroutine" — it does some work (sets variables, plays a scene, + shows a choice), then `return` sends execution back to where the jump occurred. + +### Timeline Text + +``` +# main_story.dtl +join Miko center +Miko (joy): Let's check our inventory first. +jump inventory_check/ # Push current position onto stack +Miko (smile): Back from inventory. Let's continue. + +# inventory_check.dtl +Advisor (pl5): Opening inventory... +set {coins} += 50 +Advisor (pl5): You found 50 coins! +return # Pop stack → resume main_story right after the jump +``` + +### Stack Behavior +``` +main_story stack: [] + jump inventory stack: [(main_story, idx_2)] + ── inventory_check runs ── + return stack: [] (popped) + resumes at main_story idx_3 +``` + +### Key Property +- If the stack is **empty** when `return` fires, the timeline ends +- You can jump to one sub-timeline which jumps to another, creating a deep call stack +- This is ideal for: inventory screens, crafting menus, mini-dialogues, flashbacks + +--- + +## Mechanism 3: Choice → Different Timeline + +A dialogue choice doesn't just set a variable — it directly `jump`s to an entirely different +timeline for each option. + +### Timeline Text + +``` +# crossroad.dtl (hub) +join Miko center +Advisor (doubt): Where should we go, Miko? +- The village market + Miko (joy): Let's go shopping! + jump market_scene/start + +- The dark forest + Miko (shock): Into the woods... + jump forest_scene/start + +- Stay here and rest + Miko (smile): Good idea. Let's camp. + jump camp_scene/start +``` + +### What Happens +- Player chooses "The village market" +- Events inside that branch execute (text, variables, signals) +- The `jump marketplace_scene/start` fires +- Execution moves to `market_scene.dtl` at the `start` label + +### Key Property +Each choice branch can have **multiple events** before the jump — set variables, +show reactions, play sounds — making it a full pre-transition sequence. + +--- + +## Mechanism 4: Condition → Different Timeline + +Instead of a player choice, automatic branching based on variable values can route +to different timelines. + +### Timeline Text + +``` +# auto_router.dtl +if {coins} >= 100: + Miko (joy): I'm rich! Let's go to the VIP area. + jump vip_timeline/ +elif {reputation} <= -10: + Miko (anger): They hate me here. I should leave town. + jump exile_timeline/ +else: + Miko (neutral): Just an ordinary day. + jump normal_day/ +``` + +### What Happens +- Variables `coins` and `reputation` were set by **previous timelines** +- When `auto_router` starts, Dialogic evaluates each condition +- The matching branch executes and jumps to the appropriate timeline +- If no condition matches, `else` runs (or nothing happens if no `else`) + +--- + +## Mechanism 5: Variable Set in One Timeline, Checked in Another + +The most loosely-coupled pattern. Timeline A sets a variable. Later, Timeline B +checks it with a `Condition` event. They don't jump to each other directly but +something external (code, another timeline, signal) starts Timeline B. + +### Timeline Text + +``` +# quest_start.dtl +Miko (surprise): A dragon?! I accept the quest! +set {dragon_quest_accepted} = true +set {quest_difficulty} = "hard" +[end_timeline] + +# town_square.dtl (started later by code) +if {dragon_quest_accepted}: + Merchant (happy): Heard you're hunting a dragon. Need supplies? + set {coins} -= 50 + Merchant (happy): Here's a fireproof shield! +else: + Merchant (neutral): Just browsing today? +``` + +### Key Property +- Zero timeline coupling — `town_square.dtl` doesn't know or care **which** timeline +set `{dragon_quest_accepted}` +- Variables act as a shared "world state" between all timelines +- Variable changes persist across jumps, returns, and `start_timeline()` calls +- To reset: `Dialogic.VAR.reset()` or explicitly `set {quest_accepted} = false` + +--- + +## Mechanism 6: Signal Event → GDScript → `start_timeline()` + +Timeline A emits a signal. GDScript code listens for it and starts Timeline B. +This is the most flexible pattern — the code can do anything before starting the next +timeline (load resources, change scenes, animate the world, etc.). + +### Timeline Text + +``` +# scene_cutscene.dtl +join Raptor (walk) center +Raptor (walk): Watch this transformation! +[signal arg=start_transformation] +[wait time="2.0"] +Raptor (roar): ROAR! +[end_timeline] +``` + +### GDScript + +```gdscript +# cross_timeline_gym.gd +extends Node2D + +func _ready(): + Dialogic.signal_event.connect(_on_dialogic_signal) + +func _on_dialogic_signal(argument: String): + if argument == "start_transformation": + # Do anything here: animate the world, change scenes, play music + await get_tree().create_timer(0.5).timeout + Dialogic.start_timeline("transformation_sequence") +``` + +### Signal Data Options +- **String signal:** `[signal arg="activate_portal"]` +- **Dictionary signal:** `[signal arg_type="Dictionary" arg={"target": "forest", "time": "night"}]` + +### Key Property +The signal event does **not** end the current timeline — both timelines can run if you don't +stop the first one. Use `Dialogic.end_timeline()` to explicitly end before starting a new one. + +--- + +## Mechanism 7: `do` Call Event → Autoload Method + +Similar to signal but with **typed arguments** and **method return values**. +The timeline calls a method on any autoload singleton, which can start new timelines. + +### Timeline Text + +``` +# trigger.dtl +Miko (neutral): Let me check something... +do GameManager.start_side_quest("forest_rescue", "hard") +``` + +### Autoload (GameManager.gd) + +```gdscript +extends Node + +func start_side_quest(quest_name: String, difficulty: String): + print("Starting quest: ", quest_name, " at ", difficulty) + Dialogic.start_timeline(quest_name) +``` + +### Key Difference from Signal +- `do` calls a **specific method** with typed arguments (int, string, bool, array, expression) +- `[signal]` emits a signal with a single untyped argument (string or dictionary) +- `do` is better for well-defined APIs; `[signal]` is better for loose coupling + +--- + +## Mechanism 8: GDScript `timeline_ended` Chaining + +Completely external to the timeline itself. Code listens for when Timeline A finishes, +then immediately starts Timeline B. + +### GDScript + +```gdscript +extends Node2D + +@export var timeline_sequence: Array[DialogicTimeline] = [] +var current_timeline_index := 0 + +func _ready(): + Dialogic.timeline_ended.connect(_on_timeline_ended) + Dialogic.start(timeline_sequence[0]) + +func _on_timeline_ended(): + current_timeline_index += 1 + if current_timeline_index < timeline_sequence.size(): + await get_tree().create_timer(1.0).timeout # optional pause + Dialogic.start(timeline_sequence[current_timeline_index]) +``` + +### Key Property +- The timelines themselves don't know about each other +- You can insert any logic between timelines (fades, scene loads, world changes) +- Order is determined by the exported array, editable in the Godot inspector + +--- + +## Mechanism 9: Text Input → Variable → Next Timeline + +The player types text that gets stored in a variable. A subsequent timeline +(or condition block) uses that variable to route or personalize the dialog. + +### Timeline Text + +``` +# name_entry.dtl +Miko (smile): Welcome, traveler! +Advisor (pl5): Before we begin... what should I call you? +[text_input text="Your name?" var="player_name" placeholder="Enter name..." default="Hero" allow_empty="false"] +Miko (joy): Nice to meet you, {player_name}! +jump personalized_greeting/ + +# personalized_greeting.dtl +if {player_name} == "Miko": + Miko (surprise): Hey, that's MY name! +elif {player_name} == "Dragon": + Miko (shock): Um... should I be worried? +else: + Miko (smile): {player_name}, what a wonderful name! +``` + +--- + +## Mechanism 10: Dynamic Label Jump (`jump {variable}`) + +Use a variable's value as the jump target. This is powerful for state-machine-like +narrative flow. + +### Timeline Text + +``` +# daily_cycle.dtl +# day_phase is set elsewhere: "morning", "afternoon", "evening", "night" +jump {day_phase} + +label morning +Miko (joy): Good morning! The sun is rising. + jump end_of_day + +label afternoon +Miko (neutral): It's a warm afternoon. + jump end_of_day + +label evening +Miko (shock): The sun is setting. We should hurry! + jump end_of_day + +label night +Miko (doubt): It's dark... be careful out there. + jump end_of_day + +label end_of_day +[end_timeline] +``` + +### Key Property +- The variable must resolve to a label identifier that exists in the current timeline +- Can also reference timeline: `jump {target_timeline}/{target_label}` + +--- + +## Combining Patterns: A Practical Example + +Real games combine multiple mechanisms. Here's a complex example: + +``` +# hub_world.dtl (entry point) +label hub +Miko (neutral): What should I do? +- Visit the shop → jump shop_dialogue/ +- Check quest board → jump quest_board/ +- Talk to NPCs → if {met_elder}: jump elder_chat/ + else: jump elder_intro/ +- Save and quit → [save slot="auto"] + [end_timeline] +``` + +``` +# quest_board.dtl +label start +set {viewed_board} = true +QuestGiver (happy): Here are today's quests! +- Hunt 5 wolves (reward: 50g) + set {active_quest} = "wolf_hunt" + [signal arg=quest_accepted] + return # ← back to hub + +- Gather 10 herbs (reward: 30g) + set {active_quest} = "herb_gathering" + [signal arg=quest_accepted] + return + +- Maybe later + return +``` + +The `[signal arg=quest_accepted]` is caught by GDScript which updates the world +(spawns wolves, shows quest marker) and then call `jump hub_world/hub` to return the +player to the hub. + +--- + +## Decision Guide: Which Mechanism to Use? + +| Use case | Best mechanism | +|----------|---------------| +| Narrative branches that diverge forever | **Jump to another timeline** (1) | +| Reusable mini-scenes (inventory, crafting, shop) | **Jump + Return** (2) | +| Player choice determines next scene | **Choice → Jump to timeline** (3) | +| Automatic scene routing based on game state | **Condition → Jump** (4) or **Dynamic label jump** (10) | +| World state affects dialog in another scene | **Variable set in one, checked in another** (5) | +| Cutscene triggers world/engine changes | **Signal → GDScript** (6) or **do Call** (7) | +| Linear sequence of cutscenes | **`timeline_ended` chaining** (8) | +| Player-customized content | **Text input → variable** (9) | +| Time-of-day / game phase routing | **Dynamic label jump** (10) | + +--- + +## Project Files in This Gym + +| File | Purpose | +|------|---------| +| `gym_main.dtl` | Hub timeline — presents a menu of all patterns to explore | +| `gym_pattern1_jump.dtl` | Mechanism 1: Simple cross-timeline jump | +| `gym_pattern1_target.dtl` | Target timeline for pattern 1 | +| `gym_pattern2_subroutine.dtl` | Mechanism 2: Jump → work → return | +| `gym_pattern2_caller.dtl` | Calls the subroutine | +| `gym_pattern3_variable_chain_A.dtl` | Mechanism 5: Sets a variable | +| `gym_pattern3_variable_chain_B.dtl` | Mechanism 5: Checks the variable | +| `gym_pattern4_signal_chain.dtl` | Mechanism 6: Emits a signal | +| `gym_pattern4_signal_target.dtl` | Mechanism 6: Started by signal handler | +| `gym_pattern5_choice_jump.dtl` | Mechanism 3: Choice routes to different timelines | +| `gym_pattern6_condition_router.dtl` | Mechanism 4: Variable conditions route timelines | +| `gym_pattern7_text_input.dtl` | Mechanism 9: Player input drives next timeline | +| `gym_pattern8_dynamic_label.dtl` | Mechanism 10: Variable-driven label jump | +| `gym_pattern8_morning.dtl` | Day phase: morning | +| `gym_pattern8_night.dtl` | Day phase: night | +| `gym_controller.gd` | GDScript controller with signal handlers | + +--- + +## Setup Instructions + +### 1. Create the scene in Godot editor + +The `.dtl` and `.gd` files are already in place. You just need to create the scene: + +1. Open the Godot editor for this project +2. Create a new scene: **Scene → New Scene** +3. Set the root node to **Node2D** and name it `CrossTimelineGym` +4. Attach the script: click the script icon, choose `res://docs/gyms/cross-timeline-interactions/gym_controller.gd` +5. In the Inspector, assign `start_timeline` to `res://docs/gyms/cross-timeline-interactions/gym_main.dtl` +6. Save as `res://docs/gyms/cross-timeline-interactions/gym_controller.tscn` + +### 2. Verify project settings + +The `project.godot` file has already been updated with: +- All 14 gym timelines registered in `directories/dtl_directory` +- New variables: `Gym.demo_flag`, `Gym.sub_value`, `Gym.player_name`, `Gym.time_of_day`, `Museum.coins`, `Museum.met_merchant`, `Museum.player_has_key` + +### 3. Run the scene + +Press **F6** (Run Current Scene) with `gym_controller.tscn` open to start the gym. + +### Running individual patterns + +To test a specific pattern without the hub, call from any GDScript: +```gdscript +Dialogic.start("gym_pattern1_jump") # Direct jump demo +Dialogic.start("gym_pattern2_caller", "start") # Subroutine demo +Dialogic.start("gym_pattern5_choice_jump", "start") # Choice routing demo +Dialogic.start("gym_pattern8_dynamic_label", "start") # Dynamic label demo +``` + +--- + +## Limitations & Caveats + +1. **Only one timeline runs at a time.** You cannot have two parallel timelines executing simultaneously. Dialogic is single-timeline. + +2. **Timeline references must be preregistered.** The target timeline must have its `.dtl` file listed in `project.godot` → `dialogic/directories/dtl_directory` before `jump TimelineName/` will work. + +3. **Jump stack is hidden state.** If you `jump` without `return`, the stack grows without bound. Use `Dialogic.Jump.is_jump_stack_empty()` to check. + +4. **`start_timeline()` bypasses the jump stack.** Unlike `jump`, calling `Dialogic.start_timeline()` replaces the current timeline without pushing to the stack. Use `jump` in timelines, `start_timeline()` in code. + +5. **`Dialogic.start()` vs `Dialogic.start_timeline()`.** The former also loads a layout scene (use for the first dialog in a scene). The latter only starts the timeline (use when a layout is already active). diff --git a/docs/gyms/cross-timeline-interactions/gym_controller.gd b/docs/gyms/cross-timeline-interactions/gym_controller.gd new file mode 100644 index 0000000..8db1780 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_controller.gd @@ -0,0 +1,108 @@ +# ============================================================ +# Cross-Timeline Interactions Gym — Controller +# ============================================================ +# This script demonstrates GDScript-driven cross-timeline +# interactions: signal handling, timeline chaining, and +# dynamic timeline starting. +# ============================================================ +extends Node2D + +@export var start_timeline: DialogicTimeline + + +func _ready() -> void: + # Connect to Dialogic's signal_event to react to [signal arg=...] + Dialogic.signal_event.connect(_on_dialogic_signal) + + # Connect to timeline_ended for chaining patterns + Dialogic.timeline_ended.connect(_on_timeline_ended) + + # Connect to variable changes to watch for demo variables + Dialogic.VAR.variable_changed.connect(_on_variable_changed) + + # Start the hub timeline + Dialogic.start(start_timeline) + + print("[Gym Controller] Ready. Listening for signals and timeline events.") + + +#region SIGNAL HANDLING (Pattern 4) + +# Called whenever a [signal arg=...] event fires in any timeline +func _on_dialogic_signal(argument: Variant) -> void: + print("[Gym Controller] Signal received: ", argument) + + if argument is String and argument == "demo_chain": + # Pattern 4: Signal → GDScript → New Timeline + # The signal timeline continues running, but we start a new one here. + # In a real game, you might call end_timeline() first: + # Dialogic.end_timeline() + # For this demo, we let gym_pattern4_signal_chain finish naturally + # and then start the target timeline when timeline_ended fires. + print("[Gym Controller] 'demo_chain' signal detected. Will start target timeline after current one ends.") + # We store the intent and act on timeline_ended + _pending_signal_target = "gym_pattern4_signal_target" + + elif argument is String and argument == "start_from_code": + # Example: start a specific timeline on demand + print("[Gym Controller] Starting timeline from code signal.") + Dialogic.end_timeline() + Dialogic.start_timeline("gym_pattern4_signal_target") + +#endregion + + +#region TIMELINE CHAINING (Pattern 8 extension) + +var _pending_signal_target: String = "" + + +func _on_timeline_ended() -> void: + print("[Gym Controller] Timeline ended.") + + # Pattern 4: If we had a pending signal target, start it now + if not _pending_signal_target.is_empty(): + var target := _pending_signal_target + _pending_signal_target = "" + print("[Gym Controller] Starting pending target timeline: ", target) + # Small delay so the player can see the transition + await get_tree().create_timer(0.5).timeout + Dialogic.start_timeline(target) + +#endregion + + +#region VARIABLE WATCHING (for debugging/demo) + +func _on_variable_changed(info: Dictionary) -> void: + # Only log our demo variables to avoid noise + var demo_vars := ["Gym.demo_flag", "Gym.sub_value", "Gym.player_name", + "Gym.time_of_day", "Museum.coins", "Museum.met_merchant", "Museum.player_has_key"] + if info.variable in demo_vars: + print("[Gym Controller] Variable changed: ", info.variable, " = ", info.new_value) + +#endregion + + +#region UTILITY: Start a specific pattern from code + +# You can call these from the Godot editor's remote scene tree +# or from other autoloads using: GymController.start_pattern_1() + +func start_pattern_1() -> void: + Dialogic.end_timeline() + Dialogic.start_timeline("gym_pattern1_jump") + +func start_pattern_2() -> void: + Dialogic.end_timeline() + Dialogic.start_timeline("gym_pattern2_caller", "start") + +func start_pattern_5() -> void: + Dialogic.end_timeline() + Dialogic.start_timeline("gym_pattern5_choice_jump", "start") + +func return_to_hub() -> void: + Dialogic.end_timeline() + Dialogic.start_timeline("gym_main", "menu") + +#endregion diff --git a/docs/gyms/cross-timeline-interactions/gym_controller.gd.uid b/docs/gyms/cross-timeline-interactions/gym_controller.gd.uid new file mode 100644 index 0000000..8ed9833 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_controller.gd.uid @@ -0,0 +1 @@ +uid://rc4w42wk3axw diff --git a/docs/gyms/cross-timeline-interactions/gym_main.dtl b/docs/gyms/cross-timeline-interactions/gym_main.dtl new file mode 100644 index 0000000..1cda082 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_main.dtl @@ -0,0 +1,56 @@ +# ============================================================ +# Cross-Timeline Interactions Gym — Hub +# ============================================================ +# Starting point for exploring all cross-timeline patterns. +# Pick a pattern to see it in action, then return here. +# ============================================================ + +join Miko center +join Advisor right + +Advisor (pl5): Welcome to the Cross-Timeline Interactions Gym! +Miko (joy): Here you can explore all the ways Dialogic timelines\ +can talk to each other. + +label menu + +Miko (neutral): Which pattern would you like to explore? + +- 1. Jump to another timeline + Miko (smile): Simple! Let's see a direct jump in action. + jump gym_pattern1_jump/ + +- 2. Jump + Return (subroutine) + Miko (joy): Like calling a function, but with dialog! + jump gym_pattern2_caller/start + +- 3. Variable chain (set in A, check in B) + Miko (neutral): Let's set some variables and see them carry over. + set {Gym.demo_flag} = false + jump gym_pattern3_variable_chain_A/start + +- 4. Signal → GDScript → Timeline + Miko (smile): Code listens for a signal and starts the next timeline. + [wait time="0.5"] + jump gym_pattern4_signal_chain/start + +- 5. Choice routes to different timelines + Miko (joy): Each choice jumps to a completely different scene! + jump gym_pattern5_choice_jump/start + +- 6. Condition-based routing + Miko (doubt): Variables decide which timeline runs, not the player. + jump gym_pattern6_condition_router/start + +- 7. Player text input drives the next timeline + Miko (smile): Type your name and see what happens! + jump gym_pattern7_text_input/start + +- 8. Dynamic label jump with variables + Miko (joy): The variable itself is the jump target! + jump gym_pattern8_dynamic_label/start + +- I've seen enough. Goodbye! + Miko (smile): Thanks for exploring! Come back anytime. + Advisor (pl5): Until next time. + [end_timeline] diff --git a/docs/gyms/cross-timeline-interactions/gym_main.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_main.dtl.uid new file mode 100644 index 0000000..81b4803 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_main.dtl.uid @@ -0,0 +1 @@ +uid://cclcyrwol0e7x diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl new file mode 100644 index 0000000..8f610be --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl @@ -0,0 +1,20 @@ +# ============================================================ +# Pattern 1: Direct Jump to Another Timeline +# ============================================================ +# This timeline demonstrates the simplest cross-timeline +# interaction: jumping directly to a different timeline. +# ============================================================ + +join Miko center + +Miko (neutral): I'm in gym_pattern1_jump.dtl right now. + +[wait time="1.0"] + +Miko (joy): Watch this — I'm jumping to another timeline! + +# This jumps to gym_pattern1_target.dtl at the "arrival" label +jump gym_pattern1_target/arrival + +# This line is never reached unless someone returns here +Miko (surprise): How did I get back here? Nobody called return! diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl.uid new file mode 100644 index 0000000..1d2d566 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl.uid @@ -0,0 +1 @@ +uid://wes5tngbhcr4 diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl new file mode 100644 index 0000000..ae1919c --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl @@ -0,0 +1,33 @@ +# ============================================================ +# Pattern 1 Target: Arrival Timeline +# ============================================================ +# This is the timeline we jumped TO from gym_pattern1_jump.dtl. +# It demonstrates that we can land at a specific label. +# ============================================================ + +label arrival + +join Advisor right + +Advisor (pl5): Welcome to gym_pattern1_target.dtl! +Advisor (pl5): You jumped here from gym_p_jump.dtl. +Advisor (doubt): Notice we landed at the attern1"arrival" label — not the beginning. + +Miko (surprise): That's right! I specified `jump gym_pattern1_target/arrival`. + +[wait time="1.0"] + +Advisor (pl5): The jump stack has your previous timeline saved. +Advisor (pl5): If this timeline ends naturally, there's nothing to return to. + +Miko (smile): Let's go back to the main hub now. +Miko (joy): But first, did you notice the jump syntax? + +# Tutorial note +Advisor (pl5): The syntax is `jump TimelineName/LabelName` +Advisor (doubt): To jump to the beginning, use `jump TimelineName/` + +[wait time="1.5"] + +Miko (neutral): Ready to go back? +jump gym_main/menu \ No newline at end of file diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl.uid new file mode 100644 index 0000000..367efb3 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl.uid @@ -0,0 +1 @@ +uid://lopj8f6gaa2w diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl new file mode 100644 index 0000000..7b52008 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl @@ -0,0 +1,42 @@ +# ============================================================ +# Pattern 2: Jump + Return (Subroutine Pattern) +# ============================================================ +# This timeline CALLS a sub-timeline (gym_pattern2_subroutine.dtl) +# which does some work, then RETURNS control back here. +# ============================================================ + +label start + +join Miko center +join Advisor right + +Miko (neutral): I'm in the caller timeline. +Advisor (pl5): Let me check something in the subroutine... + +# Save current position and jump to the subroutine +jump gym_pattern2_subroutine/start + +# Execution resumes HERE after the subroutine calls "return" +label after_return + +Miko (joy): Welcome back! The subroutine finished. +Advisor (pl5): Notice how we resumed right where we left off. + +[wait time="1.0"] + +# Let's demonstrate: the subroutine set a variable +if {Gym.sub_value} == 42: + Miko (smile): The subroutine set `Gym.sub_value` to {Gym.sub_value}! + Advisor (pl5): Variables set in subroutines persist after return. +else: + Miko (doubt): Hmm, the subroutine didn't set the value correctly. + Advisor (doubt): Something went wrong — `Gym.sub_value` is {Gym.sub_value}. + +[wait time="1.5"] + +Miko (neutral): That's the subroutine pattern in a nutshell! +Advisor (pl5): Jump → work → return. Powerful for reusable dialog chunks. + +[wait time="1.0"] + +jump gym_main/menu diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl.uid new file mode 100644 index 0000000..6a09670 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl.uid @@ -0,0 +1 @@ +uid://ocwrsq7faxi6 diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl new file mode 100644 index 0000000..1ebe714 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl @@ -0,0 +1,35 @@ +# ============================================================ +# Pattern 2 Subroutine: The "Called" Timeline +# ============================================================ +# This is the subroutine — a reusable dialog chunk that does +# some work and then returns to wherever it was called from. +# ============================================================ + +label start + +leave Miko +leave Advisor + +join Merchant left + +Merchant (happy): I'm the subroutine timeline! +Merchant (blink): I can do work independently and then return. + +[wait time="1.0"] + +Merchant (happy): First, let me set a variable... +set {Gym.sub_value} = 42 + +Merchant (happy): Variable `Gym.sub_value` is now 42. +Merchant (blink): The calling timeline will see this change! + +[wait time="1.0"] + +Merchant (surprise): Now watch this — I'll call `return`... + +# This pops the jump stack and resumes the caller +# right after the `jump` that called us +return + +# Nothing below here will execute +Merchant (happy): You should never see this text! diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl.uid new file mode 100644 index 0000000..983084a --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl.uid @@ -0,0 +1 @@ +uid://yyl61w1o1vpp diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl new file mode 100644 index 0000000..a405302 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl @@ -0,0 +1,24 @@ +# ============================================================ +# Pattern 3: Variable Chain (A sets, B checks) +# ============================================================ +# Timeline A sets a variable. Later, Timeline B checks it. +# The timelines don't jump to each other — the hub handles routing. +# ============================================================ + +# Timeline A: Set the variable +label start + +join Miko center + +Miko (neutral): I'm in timeline A of the variable chain. +Miko (smile): I'm going to set a variable that another timeline will check. + +# Set the flag +set {Gym.demo_flag} = true + +Miko (joy): Done! `Gym.demo_flag` is now true. +Miko (neutral): The hub will now show me timeline B to see the result. + +[wait time="1.0"] + +jump gym_pattern3_variable_chain_B/start diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl.uid new file mode 100644 index 0000000..e64839c --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl.uid @@ -0,0 +1 @@ +uid://5b27781x33d8 diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl new file mode 100644 index 0000000..27db3b4 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl @@ -0,0 +1,30 @@ +# ============================================================ +# Pattern 3: Variable Chain — Timeline B (checks the variable) +# ============================================================ + +label start + +join Advisor right + +Advisor (pl5): I'm timeline B. Let me check the variable you set... + +[wait time="1.0"] + +if {Gym.demo_flag}: + Advisor (surprise): It's TRUE! Timeline A successfully set it. + Advisor (pl5): This proves that variables persist across timeline\ + boundaries — jumps, returns, even `start_timeline()` calls. +else: + Advisor (doubt): It's false... something went wrong. + Advisor (doubt): Did you skip timeline A? + +[wait time="1.5"] + +Advisor (pl5): Key insight: variables are shared GLOBAL state. +Advisor (doubt): Any timeline can read or write any pre-declared variable. +Advisor (pl5): This makes them perfect for world state, quest flags,\ +relationship scores, and inventory. + +[wait time="1.0"] + +jump gym_main/menu diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl.uid new file mode 100644 index 0000000..5b19f5f --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl.uid @@ -0,0 +1 @@ +uid://cvvvm15gfcw1g diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl new file mode 100644 index 0000000..42ac90c --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl @@ -0,0 +1,33 @@ +# ============================================================ +# Pattern 4: Signal Event → GDScript → New Timeline +# ============================================================ +# This timeline emits a signal. GDScript in gym_controller.gd +# catches it and starts the target timeline. +# ============================================================ + +label start + +join Miko center + +Miko (neutral): This pattern uses a signal to bridge timelines. +Miko (smile): I'm going to emit a signal, and GDScript will react. + +[wait time="1.0"] + +Miko (joy): Emitting signal: "demo_chain"! + +# This emits Dialogic.signal_event("demo_chain") +# gym_controller.gd listens for this and starts +# gym_pattern4_signal_target.dtl +[signal arg=demo_chain] + +# This timeline continues until it ends... +Miko (neutral): I'll keep going while the signal fires in the background. + +[wait time="0.5"] + +Miko (smile): ...and now I'm done here! + +# That's it. The signal handler in code started +# gym_pattern4_signal_target.dtl which will take over. +[end_timeline] diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl.uid new file mode 100644 index 0000000..79800a6 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl.uid @@ -0,0 +1 @@ +uid://b18khgix70in4 diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl new file mode 100644 index 0000000..b1ad0ab --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl @@ -0,0 +1,34 @@ +# ============================================================ +# Pattern 4 Target: Started by GDScript Signal Handler +# ============================================================ +# This timeline was started by gym_controller.gd in response +# to the "demo_chain" signal from gym_pattern4_signal_chain.dtl. +# ============================================================ + +join Advisor right + +Advisor (surprise): I was started by GDScript! +Advisor (pl5): The `dialogic.signal_event` signal fired with\ +the argument "demo_chain". + +[wait time="1.0"] + +Advisor (pl5): The controller script caught it and called\ +`Dialogic.start_timeline("gym_pattern4_signal_target")`. + +Advisor (doubt): This is the most flexible pattern because... +Advisor (pl5): ...GDScript can do ANYTHING before starting the next timeline:\ +load scenes, animate sprites, play audio, check conditions... + +[wait time="2.0"] + +Advisor (pl5): Signal args can be strings (like "demo_chain")\ +or full dictionaries for complex data. + +Advisor (surprise): One caveat: `start_timeline()` doesn't use the jump stack. +Advisor (pl5): So there's no automatic `return` path. You control flow in code. + +[wait time="1.5"] + +Advisor (pl5): Ready to go back to the hub? +jump gym_main/menu diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl.uid new file mode 100644 index 0000000..9b620ab --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl.uid @@ -0,0 +1 @@ +uid://dabm67j4647sw diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl new file mode 100644 index 0000000..288110f --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl @@ -0,0 +1,37 @@ +# ============================================================ +# Pattern 5: Choice Routes to Different Timelines +# ============================================================ +# Each choice branch doesn't just set a variable — +# it JUMPS to a completely different timeline. +# ============================================================ + +label start + +join Miko center +join Advisor right + +Advisor (doubt): This is a crossroads. Each choice leads to a different\ +destination. + +Miko (neutral): Where should we go? + +- The sunny meadow + Miko (joy): I love flowers! Let's go to the meadow. + Advisor (pl5): Fresh air will do us good. + jump gym_main/menu + +- The dark cave + Miko (doubt): It's so dark in there... but I'm curious. + Advisor (doubt): Bring a torch. We don't know what lurks inside. + jump gym_main/menu + +- Back to the hub + Miko (smile): Good idea. I've seen enough crossroads. + Advisor (pl5): A safe choice. + jump gym_main/menu + +# Note: In a real game, these would jump to: +# jump meadow_scene/start +# jump cave_scene/start +# jump hub_scene/main +# But here we just return to the hub for the demo. diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl.uid new file mode 100644 index 0000000..3aeebb6 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl.uid @@ -0,0 +1 @@ +uid://cmqgipcskfvb7 diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl new file mode 100644 index 0000000..4f5563b --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl @@ -0,0 +1,39 @@ +# ============================================================ +# Pattern 6: Condition-Based Timeline Routing +# ============================================================ +# Variable values automatically decide which timeline to run. +# This is great for: time-of-day, reputation systems, quest states. +# ============================================================ + +label start + +join Miko center +join Advisor right + +Advisor (pl5): This pattern routes to different timelines\ +based on variable values. + +Miko (neutral): Let's check the variables and see where we go... + +[wait time="1.0"] + +# These variables are set/cleared by the hub menu +# and by exploring different patterns + +if {Gym.demo_flag}: + Miko (joy): `gym_demo_flag` is TRUE! You explored pattern 3. + Miko (smile): That means I'll route to the "explored" path. + jump gym_main/menu +elif {Museum.met_merchant}: + Miko (neutral): You've met the merchant before! + Advisor (pl5): We'll take a different route for returning players. + jump gym_main/menu +else: + Miko (doubt): No flags are set. This is your first time here. + Advisor (doubt): We'll take the default path. + jump gym_main/menu + +# In a real game, these would jump to different scene timelines: +# jump explored_content/ +# jump returning_player/ +# jump first_time/ diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl.uid new file mode 100644 index 0000000..4b640cd --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl.uid @@ -0,0 +1 @@ +uid://bd4jdammxoo55 diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl new file mode 100644 index 0000000..1bc2a7c --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl @@ -0,0 +1,48 @@ +# ============================================================ +# Pattern 7: Player Text Input Drives Next Timeline +# ============================================================ +# The player types text that gets stored in a variable. +# That variable then determines what happens next. +# ============================================================ + +label start + +join Miko center +join Advisor right + +Advisor (pl5): This pattern captures player text input\ +and uses it to personalize the next timeline. + +Miko (smile): Let me ask you something... + +[wait time="0.5"] + +[text_input text="What is your name, traveler?" var="Gym.player_name" placeholder="Enter your name..." default="Hero" allow_empty="false"] + +Miko (joy): {Gym.player_name}? What a great name! + +[wait time="0.5"] + +Miko (neutral): Now watch — I can use your name to route you to\ +different content. + +if {Gym.player_name} == "Miko": + Miko (surprise): Wait... that's MY name! + Advisor (surprise): An impostor? Or a coincidence? + Miko (anger): I'll be watching you, {Gym.player_name}... +elif {Gym.player_name} == "Hero": + Miko (smile): Classic choice. Very heroic! + Advisor (pl5): Predictable, but reliable. +else: + Miko (joy): {Gym.player_name}, that's unique! I've never heard that before. + Advisor (pl5): A name to remember. + +[wait time="1.5"] + +Advisor (pl5): In a real game, the name would persist across ALL timelines. +Advisor (pl5): You could reference `{Gym.player_name}` in any dialog. +Miko (smile): NPCs could greet you by name, quest logs would use it... + +[wait time="1.0"] + +jump gym_main/menu diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl.uid new file mode 100644 index 0000000..3a208ef --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl.uid @@ -0,0 +1 @@ +uid://dvlhmbl62bltq diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl b/docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl new file mode 100644 index 0000000..bbbb4fa --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl @@ -0,0 +1,72 @@ +# ============================================================ +# Pattern 8: Dynamic Label Jump (Variable as Jump Target) +# ============================================================ +# The value of a variable is used as the label to jump to. +# This creates a state-machine-like flow controlled by data. +# ============================================================ + +label start + +join Miko center + +Miko (neutral): This pattern uses a variable's VALUE as the jump target. +Miko (smile): Let me show you with a time-of-day example. + +# Set the variable that will drive the jump +# In a real game, this might be set by a clock system +set {Gym.time_of_day} = "morning" + +Miko (neutral): The variable `Gym.time_of_day` is set to "{Gym.time_of_day}". +Miko (joy): Now watch: I'll jump to a label with that exact name. + +# This jump target is resolved at runtime from the variable +jump {Gym.time_of_day} + +# ---- Labels act as states ---- + +label morning +Miko (joy): Good morning! The sun is rising over the hills. +Miko (smile): Birds are singing, and the air is crisp and fresh. +[wait time="1.5"] +jump after_demo + +label afternoon +Miko (neutral): It's a warm afternoon. The sun is high. +Miko (smile): A perfect time for adventuring! +[wait time="1.5"] +jump after_demo + +label evening +Miko (doubt): The sun is setting. Shadows grow long. +Miko (neutral): We should find shelter before night falls. +[wait time="1.5"] +jump after_demo + +label night +Miko (shock): It's dark out here! The stars are beautiful but eerie. +Miko (doubt): I can barely see the path ahead... +[wait time="1.5"] +jump after_demo + +# ---- Wrap-up ---- + +label after_demo + +join Advisor right + +Advisor (pl5): The jump target was `{Gym.time_of_day}` — resolved at runtime! +Advisor (pl5): This is incredibly powerful for: +Advisor (doubt): • Time-of-day systems +Advisor (doubt): • Quest state machines +Advisor (doubt): • Chapter/scene routing +Advisor (doubt): • Any narrative that follows a data-driven flow + +[wait time="2.0"] + +Miko (smile): You can also combine it with timeline names. +Advisor (pl5): Use one variable for the timeline and another for the label. +Advisor (surprise): Making it possible to route ANYWHERE from a variable. + +[wait time="1.5"] + +jump gym_main/menu diff --git a/docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl.uid b/docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl.uid new file mode 100644 index 0000000..aacc685 --- /dev/null +++ b/docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl.uid @@ -0,0 +1 @@ +uid://bxrdy4ww5ewbf diff --git a/docs/museums/dialogic/start_timeline.dtl b/docs/museums/dialogic/start_timeline.dtl index cd6aeb8..1417e28 100644 --- a/docs/museums/dialogic/start_timeline.dtl +++ b/docs/museums/dialogic/start_timeline.dtl @@ -8,9 +8,9 @@ # ============================================================ # ---- SETUP: Variables -------------------------------------- -set {coins} = 0 -set {player_has_key} = false -set {met_merchant} = false +set {Museum.coins} = 0 +set {Museum.player_has_key} = false +set {Museum.met_merchant} = false # ---- LABEL: opening ---------------------------------------- label opening @@ -31,7 +31,7 @@ Advisor (pl5): Remember, [color=red]colored text[/color] works as well. Advisor (pl5): [pause=1.0] ...and so do timed pauses. # ---- CONDITION: Variable-based branching ------------------ -if {coins} == 0: +if {Museum.coins} == 0: Miko (doubt): I don't have any coins yet. Let's find some! Advisor (doubt): We should look around carefully. else: @@ -39,7 +39,7 @@ else: Advisor (pl5): Good. That will come in handy. # ---- VARIABLE: Set a variable ----------------------------- -set {coins} = 10 +set {Museum.coins} = 10 Miko (joy): [shake]Look![/shake] I just found 10 coins. @@ -55,11 +55,11 @@ Merchant (happy): Welcome, traveler! Would you like to see my wares? # ---- CHOICE: Branching dialog ----------------------------- - Yes, show me what you've got! - set {met_merchant} = true + set {Museum.met_merchant} = true Miko (joy): Absolutely, show me your best items! Merchant (happy): Excellent choice! I have potions, scrolls, and enchanted blades. Merchant (blink): That will be {10/15/20} gold coins, please! - set {coins} = -10 + set {Museum.coins} = -10 Miko (shock): Here you go! That was expensive... Advisor (blink): Did you really have to spend all our coins? @@ -68,10 +68,10 @@ Merchant (happy): Welcome, traveler! Would you like to see my wares? Merchant (happy): Suit yourself. The door is always open. Advisor (pl5): A wise decision. -- Use the ancient key to barter. | [if {player_has_key}] +- Use the ancient key to barter. | [if {Museum.player_has_key}] Miko (smile): I have this ancient key. Will you trade for it? Merchant (surprise): [shake]A key from the old kingdom?![/shake] Take anything you want! - set {player_has_key} = false + set {Museum.player_has_key} = false Miko (joy): That was a great deal! Advisor (surprise): You traded the key?! That was reckless... @@ -106,7 +106,7 @@ Miko (neutral): You're right, but we have to check it out. Advisor (pl5): Very well. I'll follow your lead. # ---- CONDITION with ELSE ---------------------------------- -if {player_has_key}: +if {Museum.player_has_key}: Miko (joy): Good thing we still have the ancient key. Let's open it! Advisor (surprise): The key glows faintly as you approach. Miko (smile): Here goes nothing... diff --git a/project.godot b/project.godot index 87faea0..02ea2d8 100644 --- a/project.godot +++ b/project.godot @@ -27,6 +27,19 @@ directories/dch_directory={ "Raptor": "res://docs/museums/dialogic/Raptor.dch" } directories/dtl_directory={ +"gym_main": "res://docs/gyms/cross-timeline-interactions/gym_main.dtl", +"gym_pattern1_jump": "res://docs/gyms/cross-timeline-interactions/gym_pattern1_jump.dtl", +"gym_pattern1_target": "res://docs/gyms/cross-timeline-interactions/gym_pattern1_target.dtl", +"gym_pattern2_caller": "res://docs/gyms/cross-timeline-interactions/gym_pattern2_caller.dtl", +"gym_pattern2_subroutine": "res://docs/gyms/cross-timeline-interactions/gym_pattern2_subroutine.dtl", +"gym_pattern3_variable_chain_A": "res://docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_A.dtl", +"gym_pattern3_variable_chain_B": "res://docs/gyms/cross-timeline-interactions/gym_pattern3_variable_chain_B.dtl", +"gym_pattern4_signal_chain": "res://docs/gyms/cross-timeline-interactions/gym_pattern4_signal_chain.dtl", +"gym_pattern4_signal_target": "res://docs/gyms/cross-timeline-interactions/gym_pattern4_signal_target.dtl", +"gym_pattern5_choice_jump": "res://docs/gyms/cross-timeline-interactions/gym_pattern5_choice_jump.dtl", +"gym_pattern6_condition_router": "res://docs/gyms/cross-timeline-interactions/gym_pattern6_condition_router.dtl", +"gym_pattern7_text_input": "res://docs/gyms/cross-timeline-interactions/gym_pattern7_text_input.dtl", +"gym_pattern8_dynamic_label": "res://docs/gyms/cross-timeline-interactions/gym_pattern8_dynamic_label.dtl", "raptor_showcase": "res://docs/museums/dialogic/raptor_showcase.dtl", "start_timeline": "res://docs/museums/dialogic/start_timeline.dtl" } @@ -35,10 +48,18 @@ glossary/default_case_sensitive=true layout/style_list=[] layout/default_style="Default" variables={ +"Gym": { +"demo_flag": false, +"player_name": "Hero", +"sub_value": 0, +"time_of_day": "morning" +}, +"Museum": { "coins": 0, "met_merchant": false, "player_has_key": true } +} extensions_folder="res://addons/dialogic_additions" text/letter_speed=0.01 text/initial_text_reveal_skippable=true