yarn-spinner-runner-ts 0.1.3 → 0.1.4-b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/markup/parser.js +15 -2
- package/dist/markup/parser.js.map +1 -1
- package/dist/react/DialogueExample.js +9 -8
- package/dist/react/DialogueExample.js.map +1 -1
- package/dist/react/DialogueView.d.ts +9 -4
- package/dist/react/DialogueView.js +22 -8
- package/dist/react/DialogueView.js.map +1 -1
- package/dist/react/MarkupRenderer.js +1 -1
- package/dist/react/MarkupRenderer.js.map +1 -1
- package/dist/react/useYarnRunner.js +10 -1
- package/dist/react/useYarnRunner.js.map +1 -1
- package/dist/runtime/commands.js +12 -1
- package/dist/runtime/commands.js.map +1 -1
- package/dist/runtime/runner.d.ts +6 -0
- package/dist/runtime/runner.js +10 -0
- package/dist/runtime/runner.js.map +1 -1
- package/dist/tests/custom_functions.test.d.ts +1 -0
- package/dist/tests/custom_functions.test.js +129 -0
- package/dist/tests/custom_functions.test.js.map +1 -0
- package/dist/tests/markup.test.js +7 -0
- package/dist/tests/markup.test.js.map +1 -1
- package/dist/tests/story_end.test.d.ts +1 -0
- package/dist/tests/story_end.test.js +37 -0
- package/dist/tests/story_end.test.js.map +1 -0
- package/docs/markup.md +33 -33
- package/eslint.config.cjs +39 -39
- package/package.json +1 -1
- package/src/markup/parser.ts +56 -43
- package/src/react/DialogueExample.tsx +12 -13
- package/src/react/DialogueView.tsx +298 -275
- package/src/react/MarkupRenderer.tsx +1 -2
- package/src/react/useYarnRunner.tsx +13 -1
- package/src/runtime/commands.ts +14 -1
- package/src/runtime/runner.ts +12 -0
- package/src/tests/custom_functions.test.ts +140 -0
- package/src/tests/markup.test.ts +17 -1
- package/src/tests/story_end.test.ts +42 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useState, useMemo } from "react";
|
|
2
2
|
import { parseYarn } from "../parse/parser.js";
|
|
3
3
|
import { compile } from "../compile/compiler.js";
|
|
4
|
-
import { useYarnRunner } from "./useYarnRunner.js";
|
|
5
4
|
import { DialogueView } from "./DialogueView.js";
|
|
6
5
|
import { parseScenes } from "../scene/parser.js";
|
|
7
6
|
import type { SceneCollection } from "../scene/types.js";
|
|
@@ -9,8 +8,7 @@ import type { SceneCollection } from "../scene/types.js";
|
|
|
9
8
|
const DEFAULT_YARN = `title: Start
|
|
10
9
|
scene: scene1
|
|
11
10
|
---
|
|
12
|
-
Narrator:
|
|
13
|
-
Narrator: Welcome to yarn-spinner-ts!
|
|
11
|
+
Narrator: Welcome to [b]yarn-spinner-ts[/b]!
|
|
14
12
|
npc: This is a dialogue system powered by Yarn Spinner.
|
|
15
13
|
Narrator: Click anywhere to continue, or choose an option below.
|
|
16
14
|
-> Start the adventure &css{backgroundColor: #4a9eff; color: white;}
|
|
@@ -47,7 +45,7 @@ actors:
|
|
|
47
45
|
export function DialogueExample() {
|
|
48
46
|
const [yarnText] = useState(DEFAULT_YARN);
|
|
49
47
|
const [error, setError] = useState<string | null>(null);
|
|
50
|
-
const enableTypingAnimation =
|
|
48
|
+
const enableTypingAnimation = false;
|
|
51
49
|
|
|
52
50
|
const scenes: SceneCollection = useMemo(() => {
|
|
53
51
|
try {
|
|
@@ -69,13 +67,10 @@ export function DialogueExample() {
|
|
|
69
67
|
}
|
|
70
68
|
}, [yarnText]);
|
|
71
69
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
variables: {},
|
|
77
|
-
}
|
|
78
|
-
);
|
|
70
|
+
const customFunctions = useMemo(() => ({
|
|
71
|
+
greet: () => {console.log('test')},
|
|
72
|
+
double: (num: unknown) => Number(num) * 2
|
|
73
|
+
}), []);
|
|
79
74
|
|
|
80
75
|
return (
|
|
81
76
|
<div
|
|
@@ -106,8 +101,8 @@ export function DialogueExample() {
|
|
|
106
101
|
)}
|
|
107
102
|
|
|
108
103
|
<DialogueView
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
program={program || { nodes: {}, enums: {} }}
|
|
105
|
+
startNode="Start"
|
|
111
106
|
scenes={scenes}
|
|
112
107
|
enableTypingAnimation={enableTypingAnimation}
|
|
113
108
|
showTypingCursor={true}
|
|
@@ -117,6 +112,10 @@ export function DialogueExample() {
|
|
|
117
112
|
autoAdvanceDelay={2000}
|
|
118
113
|
actorTransitionDuration={1000}
|
|
119
114
|
pauseBeforeAdvance={enableTypingAnimation ? 1000 : 0}
|
|
115
|
+
onStoryEnd={(info) => {
|
|
116
|
+
console.log('Story ended with variables:', info.variables);
|
|
117
|
+
}}
|
|
118
|
+
functions={customFunctions}
|
|
120
119
|
/>
|
|
121
120
|
</div>
|
|
122
121
|
</div>
|
|
@@ -1,275 +1,298 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { MarkupRenderer } from "./MarkupRenderer.js";
|
|
7
|
-
// Note: CSS is imported in the browser demo entry point (examples/browser/main.tsx)
|
|
8
|
-
// This prevents Node.js from trying to resolve CSS imports during tests
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
currentRule += char;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
cleanValue = cleanValue.slice(
|
|
78
|
-
}
|
|
79
|
-
(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { DialogueScene } from "./DialogueScene.js";
|
|
3
|
+
import type { SceneCollection } from "../scene/types.js";
|
|
4
|
+
import { TypingText } from "./TypingText.js";
|
|
5
|
+
import { useYarnRunner } from "./useYarnRunner.js";
|
|
6
|
+
import { MarkupRenderer } from "./MarkupRenderer.js";
|
|
7
|
+
// Note: CSS is imported in the browser demo entry point (examples/browser/main.tsx)
|
|
8
|
+
// This prevents Node.js from trying to resolve CSS imports during tests
|
|
9
|
+
|
|
10
|
+
import type { IRProgram } from "../compile/ir.js";
|
|
11
|
+
|
|
12
|
+
export interface DialogueViewProps {
|
|
13
|
+
program: IRProgram;
|
|
14
|
+
startNode?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
scenes?: SceneCollection;
|
|
17
|
+
actorTransitionDuration?: number;
|
|
18
|
+
// Custom functions and callbacks
|
|
19
|
+
functions?: Record<string, (...args: unknown[]) => unknown>;
|
|
20
|
+
onStoryEnd?: (info: { variables: Readonly<Record<string, unknown>>; storyEnd: true }) => void;
|
|
21
|
+
// Typing animation options
|
|
22
|
+
enableTypingAnimation?: boolean;
|
|
23
|
+
typingSpeed?: number;
|
|
24
|
+
showTypingCursor?: boolean;
|
|
25
|
+
cursorCharacter?: string;
|
|
26
|
+
// Auto-advance after typing completes
|
|
27
|
+
autoAdvanceAfterTyping?: boolean;
|
|
28
|
+
autoAdvanceDelay?: number; // Delay in ms after typing completes before auto-advancing
|
|
29
|
+
// Pause before advance
|
|
30
|
+
pauseBeforeAdvance?: number; // Delay in ms before advancing when clicking (0 = no pause)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Helper to parse CSS string into object
|
|
34
|
+
function parseCss(cssStr: string | undefined): React.CSSProperties {
|
|
35
|
+
if (!cssStr) return {};
|
|
36
|
+
const styles: React.CSSProperties = {};
|
|
37
|
+
// Improved parser: handles quoted values and commas
|
|
38
|
+
// Split by semicolon, but preserve quoted strings
|
|
39
|
+
const rules: string[] = [];
|
|
40
|
+
let currentRule = "";
|
|
41
|
+
let inQuotes = false;
|
|
42
|
+
let quoteChar = "";
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < cssStr.length; i++) {
|
|
45
|
+
const char = cssStr[i];
|
|
46
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
47
|
+
inQuotes = true;
|
|
48
|
+
quoteChar = char;
|
|
49
|
+
currentRule += char;
|
|
50
|
+
} else if (char === quoteChar && inQuotes) {
|
|
51
|
+
inQuotes = false;
|
|
52
|
+
quoteChar = "";
|
|
53
|
+
currentRule += char;
|
|
54
|
+
} else if (char === ";" && !inQuotes) {
|
|
55
|
+
rules.push(currentRule.trim());
|
|
56
|
+
currentRule = "";
|
|
57
|
+
} else {
|
|
58
|
+
currentRule += char;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (currentRule.trim()) {
|
|
62
|
+
rules.push(currentRule.trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
rules.forEach((rule) => {
|
|
66
|
+
if (!rule) return;
|
|
67
|
+
const colonIndex = rule.indexOf(":");
|
|
68
|
+
if (colonIndex === -1) return;
|
|
69
|
+
const prop = rule.slice(0, colonIndex).trim();
|
|
70
|
+
const value = rule.slice(colonIndex + 1).trim();
|
|
71
|
+
if (prop && value) {
|
|
72
|
+
// Convert kebab-case to camelCase
|
|
73
|
+
const camelProp = prop.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
74
|
+
// Remove quotes from value if present, and strip !important (React doesn't support it)
|
|
75
|
+
let cleanValue = value.trim();
|
|
76
|
+
if (cleanValue.endsWith("!important")) {
|
|
77
|
+
cleanValue = cleanValue.slice(0, -10).trim();
|
|
78
|
+
}
|
|
79
|
+
if (cleanValue.startsWith('"') && cleanValue.endsWith('"')) {
|
|
80
|
+
cleanValue = cleanValue.slice(1, -1);
|
|
81
|
+
} else if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
|
|
82
|
+
cleanValue = cleanValue.slice(1, -1);
|
|
83
|
+
}
|
|
84
|
+
(styles as any)[camelProp] = cleanValue;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return styles;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function DialogueView({
|
|
91
|
+
program,
|
|
92
|
+
startNode = "Start",
|
|
93
|
+
className,
|
|
94
|
+
scenes,
|
|
95
|
+
actorTransitionDuration = 350,
|
|
96
|
+
functions,
|
|
97
|
+
onStoryEnd,
|
|
98
|
+
enableTypingAnimation = false,
|
|
99
|
+
typingSpeed = 50, // Characters per second (50 cps = ~20ms per character)
|
|
100
|
+
showTypingCursor = true,
|
|
101
|
+
cursorCharacter = "|",
|
|
102
|
+
autoAdvanceAfterTyping = false,
|
|
103
|
+
autoAdvanceDelay = 500,
|
|
104
|
+
pauseBeforeAdvance = 0,
|
|
105
|
+
}: DialogueViewProps) {
|
|
106
|
+
const { result, advance } = useYarnRunner(program, {
|
|
107
|
+
startAt: startNode,
|
|
108
|
+
functions,
|
|
109
|
+
variables: {},
|
|
110
|
+
onStoryEnd,
|
|
111
|
+
});
|
|
112
|
+
const sceneName = result?.type === "text" || result?.type === "options" ? result.scene : undefined;
|
|
113
|
+
const speaker = result?.type === "text" ? result.speaker : undefined;
|
|
114
|
+
const sceneCollection = scenes || { scenes: {} };
|
|
115
|
+
|
|
116
|
+
const [typingComplete, setTypingComplete] = useState(false);
|
|
117
|
+
const [currentTextKey, setCurrentTextKey] = useState(0);
|
|
118
|
+
const [skipTyping, setSkipTyping] = useState(false);
|
|
119
|
+
const advanceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!result || result.type !== "command") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const timer = setTimeout(() => advance(), 50);
|
|
126
|
+
return () => clearTimeout(timer);
|
|
127
|
+
}, [result, advance]);
|
|
128
|
+
|
|
129
|
+
// Reset typing completion when text changes
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (result?.type === "text") {
|
|
132
|
+
setTypingComplete(false);
|
|
133
|
+
setSkipTyping(false);
|
|
134
|
+
setCurrentTextKey((prev) => prev + 1); // Force re-render of TypingText
|
|
135
|
+
}
|
|
136
|
+
// Cleanup any pending advance timeouts when text changes
|
|
137
|
+
return () => {
|
|
138
|
+
if (advanceTimeoutRef.current) {
|
|
139
|
+
clearTimeout(advanceTimeoutRef.current);
|
|
140
|
+
advanceTimeoutRef.current = null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}, [result?.type === "text" ? result.text : null]);
|
|
144
|
+
|
|
145
|
+
// Handle auto-advance after typing completes
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (
|
|
148
|
+
autoAdvanceAfterTyping &&
|
|
149
|
+
typingComplete &&
|
|
150
|
+
result?.type === "text" &&
|
|
151
|
+
!result.isDialogueEnd
|
|
152
|
+
) {
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
advance();
|
|
155
|
+
}, autoAdvanceDelay);
|
|
156
|
+
return () => clearTimeout(timer);
|
|
157
|
+
}
|
|
158
|
+
}, [autoAdvanceAfterTyping, typingComplete, result, advance, autoAdvanceDelay]);
|
|
159
|
+
|
|
160
|
+
if (!result) {
|
|
161
|
+
return (
|
|
162
|
+
<div className={`yd-empty ${className || ""}`}>
|
|
163
|
+
<p>Dialogue ended or not started.</p>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (result.type === "text") {
|
|
169
|
+
const nodeStyles = parseCss(result.nodeCss);
|
|
170
|
+
const displayText = result.text || "\u00A0";
|
|
171
|
+
const shouldShowContinue = !result.isDialogueEnd && !enableTypingAnimation;
|
|
172
|
+
|
|
173
|
+
// Handle story end and call onStoryEnd if provided
|
|
174
|
+
if (result.isDialogueEnd && onStoryEnd && 'variables' in result) {
|
|
175
|
+
onStoryEnd({
|
|
176
|
+
variables: result.variables as Readonly<Record<string, unknown>>,
|
|
177
|
+
storyEnd: true
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const handleClick = () => {
|
|
182
|
+
if (result.isDialogueEnd) return;
|
|
183
|
+
|
|
184
|
+
// If typing is in progress, skip it; otherwise advance
|
|
185
|
+
if (enableTypingAnimation && !typingComplete) {
|
|
186
|
+
// Skip typing animation
|
|
187
|
+
setSkipTyping(true);
|
|
188
|
+
setTypingComplete(true);
|
|
189
|
+
} else {
|
|
190
|
+
// Clear any pending timeout
|
|
191
|
+
if (advanceTimeoutRef.current) {
|
|
192
|
+
clearTimeout(advanceTimeoutRef.current);
|
|
193
|
+
advanceTimeoutRef.current = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Apply pause before advance if configured
|
|
197
|
+
if (pauseBeforeAdvance > 0) {
|
|
198
|
+
advanceTimeoutRef.current = setTimeout(() => {
|
|
199
|
+
advance();
|
|
200
|
+
advanceTimeoutRef.current = null;
|
|
201
|
+
}, pauseBeforeAdvance);
|
|
202
|
+
} else {
|
|
203
|
+
advance();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="yd-container">
|
|
210
|
+
<DialogueScene
|
|
211
|
+
sceneName={sceneName}
|
|
212
|
+
speaker={speaker}
|
|
213
|
+
scenes={sceneCollection}
|
|
214
|
+
actorTransitionDuration={actorTransitionDuration}
|
|
215
|
+
/>
|
|
216
|
+
<div
|
|
217
|
+
className={`yd-dialogue-box ${result.isDialogueEnd ? "yd-text-box-end" : ""} ${className || ""}`}
|
|
218
|
+
style={nodeStyles} // Only apply dynamic node CSS
|
|
219
|
+
onClick={handleClick}
|
|
220
|
+
>
|
|
221
|
+
<div className="yd-text-box">
|
|
222
|
+
{result.speaker && (
|
|
223
|
+
<div className="yd-speaker">
|
|
224
|
+
{result.speaker}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
<p className={`yd-text ${result.speaker ? "yd-text-with-speaker" : ""}`}>
|
|
228
|
+
{enableTypingAnimation ? (
|
|
229
|
+
<TypingText
|
|
230
|
+
key={currentTextKey}
|
|
231
|
+
text={displayText}
|
|
232
|
+
markup={result.markup}
|
|
233
|
+
typingSpeed={typingSpeed}
|
|
234
|
+
showCursor={showTypingCursor}
|
|
235
|
+
cursorCharacter={cursorCharacter}
|
|
236
|
+
disabled={skipTyping}
|
|
237
|
+
onComplete={() => setTypingComplete(true)}
|
|
238
|
+
/>
|
|
239
|
+
) : (
|
|
240
|
+
<MarkupRenderer text={displayText} markup={result.markup} />
|
|
241
|
+
)}
|
|
242
|
+
</p>
|
|
243
|
+
{shouldShowContinue && (
|
|
244
|
+
<div className="yd-continue">
|
|
245
|
+
▼
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (result.type === "options") {
|
|
255
|
+
const nodeStyles = parseCss(result.nodeCss);
|
|
256
|
+
return (
|
|
257
|
+
<div className="yd-container">
|
|
258
|
+
<DialogueScene
|
|
259
|
+
sceneName={sceneName}
|
|
260
|
+
speaker={speaker}
|
|
261
|
+
scenes={sceneCollection}
|
|
262
|
+
actorTransitionDuration={actorTransitionDuration}
|
|
263
|
+
/>
|
|
264
|
+
<div className={`yd-options-container ${className || ""}`}>
|
|
265
|
+
<div className="yd-options-box" style={nodeStyles}>
|
|
266
|
+
<div className="yd-options-title">Choose an option:</div>
|
|
267
|
+
<div className="yd-options-list">
|
|
268
|
+
{result.options.map((option, index) => {
|
|
269
|
+
const optionStyles = parseCss(option.css);
|
|
270
|
+
return (
|
|
271
|
+
<button
|
|
272
|
+
key={index}
|
|
273
|
+
className="yd-option-button"
|
|
274
|
+
onClick={() => advance(index)}
|
|
275
|
+
style={optionStyles} // Only apply dynamic option CSS
|
|
276
|
+
>
|
|
277
|
+
<MarkupRenderer text={option.text} markup={option.markup} />
|
|
278
|
+
</button>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Command result - auto-advance
|
|
289
|
+
if (result.type === "command") {
|
|
290
|
+
return (
|
|
291
|
+
<div className={`yd-command ${className || ""}`}>
|
|
292
|
+
<p>Executing: {result.command}</p>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return null;
|
|
298
|
+
}
|