488 lines
16 KiB
Markdown
488 lines
16 KiB
Markdown
# 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).
|