@rohal12/spindle 0.18.1 → 0.20.0
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/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/components/App.tsx +7 -1
- package/src/components/Passage.tsx +3 -1
- package/src/components/TriggerDialogHost.tsx +42 -0
- package/src/components/macros/Button.tsx +25 -10
- package/src/components/macros/Dialog.tsx +6 -12
- package/src/components/macros/MacroLink.tsx +24 -36
- package/src/components/macros/PassageDisplay.tsx +255 -7
- package/src/components/macros/Watch.tsx +75 -0
- package/src/index.tsx +16 -0
- package/src/macros/register-builtins.ts +1 -0
- package/src/store.ts +32 -0
- package/src/story-api.ts +29 -0
- package/src/story-init.ts +15 -48
- package/src/styles.css +43 -1
- package/src/transition.ts +69 -0
- package/src/triggers.ts +161 -0
package/src/store.ts
CHANGED
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
type Patch,
|
|
8
8
|
} from 'immer';
|
|
9
9
|
import type { StoryData } from './parser';
|
|
10
|
+
import type { TransitionConfig } from './transition';
|
|
10
11
|
import type { SavePayload, SaveHistoryMoment } from './saves/types';
|
|
11
12
|
import { executeStoryInit } from './story-init';
|
|
13
|
+
import { resetTriggers } from './triggers';
|
|
12
14
|
import {
|
|
13
15
|
initSaveSystem,
|
|
14
16
|
startNewPlaythrough,
|
|
@@ -201,6 +203,8 @@ export interface StoryState {
|
|
|
201
203
|
maxHistory: number;
|
|
202
204
|
saveError: string | null;
|
|
203
205
|
loadError: string | null;
|
|
206
|
+
transitionConfig: TransitionConfig | null;
|
|
207
|
+
nextTransition: TransitionConfig | null;
|
|
204
208
|
|
|
205
209
|
setMaxHistory: (limit: number) => void;
|
|
206
210
|
init: (
|
|
@@ -222,6 +226,9 @@ export interface StoryState {
|
|
|
222
226
|
getSavePayload: () => SavePayload;
|
|
223
227
|
loadFromPayload: (payload: SavePayload) => void;
|
|
224
228
|
getHistoryVariables: (index: number) => Record<string, unknown>;
|
|
229
|
+
setTransition: (config: TransitionConfig | null) => void;
|
|
230
|
+
setNextTransition: (config: TransitionConfig | null) => void;
|
|
231
|
+
consumeNextTransition: () => TransitionConfig | null;
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
export const useStoryStore = create<StoryState>()(
|
|
@@ -240,6 +247,8 @@ export const useStoryStore = create<StoryState>()(
|
|
|
240
247
|
maxHistory: 40,
|
|
241
248
|
saveError: null,
|
|
242
249
|
loadError: null,
|
|
250
|
+
transitionConfig: null,
|
|
251
|
+
nextTransition: null,
|
|
243
252
|
|
|
244
253
|
setMaxHistory: (limit: number) => {
|
|
245
254
|
set((state) => {
|
|
@@ -441,6 +450,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
441
450
|
if (!startPassage) return;
|
|
442
451
|
|
|
443
452
|
resetPRNG();
|
|
453
|
+
resetTriggers();
|
|
444
454
|
const initialVars = deepClone(variableDefaults);
|
|
445
455
|
resetModuleState(deepClone(initialVars));
|
|
446
456
|
|
|
@@ -614,5 +624,27 @@ export const useStoryStore = create<StoryState>()(
|
|
|
614
624
|
getHistoryVariables: (index: number): Record<string, unknown> => {
|
|
615
625
|
return deepClone(reconstructVarsAt(index));
|
|
616
626
|
},
|
|
627
|
+
|
|
628
|
+
setTransition: (config: TransitionConfig | null) => {
|
|
629
|
+
set((state) => {
|
|
630
|
+
state.transitionConfig = config as TransitionConfig | null;
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
setNextTransition: (config: TransitionConfig | null) => {
|
|
635
|
+
set((state) => {
|
|
636
|
+
state.nextTransition = config as TransitionConfig | null;
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
consumeNextTransition: (): TransitionConfig | null => {
|
|
641
|
+
const current = get().nextTransition;
|
|
642
|
+
if (current !== null) {
|
|
643
|
+
set((state) => {
|
|
644
|
+
state.nextTransition = null;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return current;
|
|
648
|
+
},
|
|
617
649
|
})),
|
|
618
650
|
);
|
package/src/story-api.ts
CHANGED
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
randomInt,
|
|
22
22
|
snapshotPRNG,
|
|
23
23
|
} from './prng';
|
|
24
|
+
import { addTrigger, removeTrigger } from './triggers';
|
|
25
|
+
import type { WatchOptions } from './triggers';
|
|
26
|
+
import type { TransitionConfig } from './transition';
|
|
24
27
|
|
|
25
28
|
export type { StoryAction };
|
|
26
29
|
|
|
@@ -65,6 +68,13 @@ export interface StoryAPI {
|
|
|
65
68
|
on(event: 'actionsChanged', callback: ActionsChangedCallback): () => void;
|
|
66
69
|
on(event: 'variableChanged', callback: VariableChangedCallback): () => void;
|
|
67
70
|
waitForActions(): Promise<StoryAction[]>;
|
|
71
|
+
watch(
|
|
72
|
+
condition: string,
|
|
73
|
+
callbackOrOptions: (() => void) | WatchOptions,
|
|
74
|
+
): () => void;
|
|
75
|
+
unwatch(name: string): void;
|
|
76
|
+
setTransition(config: TransitionConfig | null): void;
|
|
77
|
+
setNextTransition(config: TransitionConfig | null): void;
|
|
68
78
|
random(): number;
|
|
69
79
|
randomInt(min: number, max: number): number;
|
|
70
80
|
readonly config: {
|
|
@@ -263,6 +273,25 @@ function createStoryAPI(): StoryAPI {
|
|
|
263
273
|
});
|
|
264
274
|
},
|
|
265
275
|
|
|
276
|
+
watch(
|
|
277
|
+
condition: string,
|
|
278
|
+
callbackOrOptions: (() => void) | WatchOptions,
|
|
279
|
+
): () => void {
|
|
280
|
+
return addTrigger(condition, callbackOrOptions);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
unwatch(name: string): void {
|
|
284
|
+
removeTrigger(name);
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
setTransition(config: TransitionConfig | null): void {
|
|
288
|
+
useStoryStore.getState().setTransition(config);
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
setNextTransition(config: TransitionConfig | null): void {
|
|
292
|
+
useStoryStore.getState().setNextTransition(config);
|
|
293
|
+
},
|
|
294
|
+
|
|
266
295
|
random(): number {
|
|
267
296
|
return random();
|
|
268
297
|
},
|
package/src/story-init.ts
CHANGED
|
@@ -1,63 +1,30 @@
|
|
|
1
|
+
import { h, render } from 'preact';
|
|
1
2
|
import { useStoryStore } from './store';
|
|
2
3
|
import { tokenize } from './markup/tokenizer';
|
|
3
4
|
import { buildAST } from './markup/ast';
|
|
4
|
-
import {
|
|
5
|
-
import type { ASTNode } from './markup/ast';
|
|
5
|
+
import { renderNodes } from './markup/render';
|
|
6
6
|
import { setSaveTitlePassage } from './saves/save-manager';
|
|
7
|
-
import { deepClone } from './class-registry';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
function walkAndExecute(
|
|
14
|
-
nodes: ASTNode[],
|
|
15
|
-
vars: Record<string, unknown>,
|
|
16
|
-
temps: Record<string, unknown>,
|
|
17
|
-
) {
|
|
18
|
-
for (const node of nodes) {
|
|
19
|
-
if (node.type !== 'macro') continue;
|
|
20
|
-
|
|
21
|
-
if (node.name === 'set') {
|
|
22
|
-
execute(node.rawArgs, vars, temps);
|
|
23
|
-
} else if (node.name === 'do') {
|
|
24
|
-
const code = node.children
|
|
25
|
-
.map((n) => (n.type === 'text' ? n.value : ''))
|
|
26
|
-
.join('');
|
|
27
|
-
execute(code, vars, temps);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Execute the StoryInit passage: tokenize, parse, and run all {set}/{do} macros.
|
|
34
|
-
* Called during boot and after restart() to ensure variables are initialized.
|
|
9
|
+
* Execute the StoryInit passage: tokenize, parse, and render all macros
|
|
10
|
+
* into a detached DOM node so their side effects fire through the normal
|
|
11
|
+
* Preact pipeline. This is macro-agnostic — any macro works in StoryInit.
|
|
35
12
|
*/
|
|
36
13
|
export function executeStoryInit() {
|
|
37
14
|
const state = useStoryStore.getState();
|
|
38
15
|
if (!state.storyData) return;
|
|
39
16
|
|
|
40
17
|
const storyInit = state.storyData.passages.get('StoryInit');
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Apply all changes to the store
|
|
52
|
-
for (const key of Object.keys(vars)) {
|
|
53
|
-
if (vars[key] !== state.variables[key]) {
|
|
54
|
-
state.setVariable(key, vars[key]);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
for (const key of Object.keys(temps)) {
|
|
58
|
-
if (temps[key] !== state.temporary[key]) {
|
|
59
|
-
state.setTemporary(key, temps[key]);
|
|
60
|
-
}
|
|
18
|
+
if (storyInit) {
|
|
19
|
+
const tokens = tokenize(storyInit.content);
|
|
20
|
+
const ast = buildAST(tokens);
|
|
21
|
+
|
|
22
|
+
const container = document.createElement('div');
|
|
23
|
+
render(
|
|
24
|
+
h(() => renderNodes(ast) as any, null),
|
|
25
|
+
container,
|
|
26
|
+
);
|
|
27
|
+
render(null, container);
|
|
61
28
|
}
|
|
62
29
|
|
|
63
30
|
// Register SaveTitle passage if it exists
|
package/src/styles.css
CHANGED
|
@@ -29,8 +29,32 @@ tw-storydata {
|
|
|
29
29
|
padding: 2em 1.5em;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* Passage transitions */
|
|
33
|
+
|
|
34
|
+
.passage-container {
|
|
35
|
+
position: relative;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.passage-container--crossfading {
|
|
39
|
+
display: grid;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.passage-container--crossfading > * {
|
|
43
|
+
grid-area: 1 / 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
.passage {
|
|
33
|
-
animation: passage-fade-in 0.3s ease
|
|
47
|
+
animation: passage-fade-in var(--passage-in-duration, 0.3s) ease;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.passage[data-transition='none'] {
|
|
51
|
+
animation: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.passage-snapshot {
|
|
55
|
+
animation: passage-fade-out var(--passage-out-duration, 0.3s) ease forwards;
|
|
56
|
+
pointer-events: none;
|
|
57
|
+
user-select: none;
|
|
34
58
|
}
|
|
35
59
|
|
|
36
60
|
@keyframes passage-fade-in {
|
|
@@ -44,6 +68,24 @@ tw-storydata {
|
|
|
44
68
|
}
|
|
45
69
|
}
|
|
46
70
|
|
|
71
|
+
@keyframes passage-fade-out {
|
|
72
|
+
from {
|
|
73
|
+
opacity: 1;
|
|
74
|
+
transform: translateY(0);
|
|
75
|
+
}
|
|
76
|
+
to {
|
|
77
|
+
opacity: 0;
|
|
78
|
+
transform: translateY(-8px);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@media (prefers-reduced-motion: reduce) {
|
|
83
|
+
.passage,
|
|
84
|
+
.passage-snapshot {
|
|
85
|
+
animation-duration: 0.01s !important;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
47
89
|
.macro-button {
|
|
48
90
|
background: rgba(100, 181, 246, 0.15);
|
|
49
91
|
color: #64b5f6;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type TransitionType = 'none' | 'fade' | 'fade-through' | 'crossfade';
|
|
2
|
+
|
|
3
|
+
export interface TransitionConfig {
|
|
4
|
+
type: TransitionType;
|
|
5
|
+
duration?: number;
|
|
6
|
+
pause?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ResolvedTransition = Required<TransitionConfig>;
|
|
10
|
+
|
|
11
|
+
const TRANSITION_TYPES = new Set<TransitionType>([
|
|
12
|
+
'none',
|
|
13
|
+
'fade',
|
|
14
|
+
'fade-through',
|
|
15
|
+
'crossfade',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export const BUILT_IN_DEFAULT: ResolvedTransition = {
|
|
19
|
+
type: 'fade-through',
|
|
20
|
+
duration: 300,
|
|
21
|
+
pause: 50,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function resolveTransitionFromTags(
|
|
25
|
+
tags: string[],
|
|
26
|
+
): TransitionConfig | null {
|
|
27
|
+
const typeTag = tags.find((t) => t.startsWith('transition:'));
|
|
28
|
+
if (!typeTag) return null;
|
|
29
|
+
|
|
30
|
+
const rawType = typeTag.slice('transition:'.length);
|
|
31
|
+
if (!TRANSITION_TYPES.has(rawType as TransitionType)) {
|
|
32
|
+
console.warn(`Unknown transition type: "${rawType}"`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const config: TransitionConfig = { type: rawType as TransitionType };
|
|
37
|
+
|
|
38
|
+
for (const tag of tags) {
|
|
39
|
+
if (tag.startsWith('duration:')) {
|
|
40
|
+
const n = Number(tag.slice('duration:'.length));
|
|
41
|
+
if (!Number.isNaN(n)) config.duration = n;
|
|
42
|
+
} else if (tag.startsWith('pause:')) {
|
|
43
|
+
const n = Number(tag.slice('pause:'.length));
|
|
44
|
+
if (!Number.isNaN(n)) config.pause = n;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function fillDefaults(partial: TransitionConfig): ResolvedTransition {
|
|
52
|
+
return {
|
|
53
|
+
type: partial.type,
|
|
54
|
+
duration: partial.duration ?? BUILT_IN_DEFAULT.duration,
|
|
55
|
+
pause: partial.pause ?? BUILT_IN_DEFAULT.pause,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveTransition(
|
|
60
|
+
targetTags: string[],
|
|
61
|
+
nextTransition: TransitionConfig | null,
|
|
62
|
+
storeDefault: TransitionConfig | null,
|
|
63
|
+
): ResolvedTransition {
|
|
64
|
+
const fromTags = resolveTransitionFromTags(targetTags);
|
|
65
|
+
if (fromTags) return fillDefaults(fromTags);
|
|
66
|
+
if (nextTransition) return fillDefaults(nextTransition);
|
|
67
|
+
if (storeDefault) return fillDefaults(storeDefault);
|
|
68
|
+
return { ...BUILT_IN_DEFAULT };
|
|
69
|
+
}
|
package/src/triggers.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { evaluate, execute } from './expression';
|
|
2
|
+
import { useStoryStore } from './store';
|
|
3
|
+
|
|
4
|
+
export interface WatchOptions {
|
|
5
|
+
goto?: string;
|
|
6
|
+
dialog?: string;
|
|
7
|
+
run?: string;
|
|
8
|
+
once?: boolean;
|
|
9
|
+
name?: string;
|
|
10
|
+
priority?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Trigger {
|
|
14
|
+
id: number;
|
|
15
|
+
name?: string;
|
|
16
|
+
condition: string;
|
|
17
|
+
callback?: () => void;
|
|
18
|
+
options?: WatchOptions;
|
|
19
|
+
lastResult: boolean;
|
|
20
|
+
priority: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let nextId = 0;
|
|
24
|
+
let triggers: Trigger[] = [];
|
|
25
|
+
let checking = false;
|
|
26
|
+
|
|
27
|
+
type DialogCallback = () => void;
|
|
28
|
+
let dialogQueue: string[] = [];
|
|
29
|
+
let dialogNotify: DialogCallback | null = null;
|
|
30
|
+
|
|
31
|
+
function evalCondition(condition: string): boolean {
|
|
32
|
+
const state = useStoryStore.getState();
|
|
33
|
+
try {
|
|
34
|
+
return !!evaluate(condition, state.variables, state.temporary);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function addTrigger(
|
|
41
|
+
condition: string,
|
|
42
|
+
callbackOrOptions: (() => void) | WatchOptions,
|
|
43
|
+
): () => void {
|
|
44
|
+
const id = nextId++;
|
|
45
|
+
const isCallback = typeof callbackOrOptions === 'function';
|
|
46
|
+
const options = isCallback ? undefined : callbackOrOptions;
|
|
47
|
+
const callback = isCallback ? callbackOrOptions : undefined;
|
|
48
|
+
|
|
49
|
+
const trigger: Trigger = {
|
|
50
|
+
id,
|
|
51
|
+
name: options?.name,
|
|
52
|
+
condition,
|
|
53
|
+
callback,
|
|
54
|
+
options,
|
|
55
|
+
lastResult: evalCondition(condition),
|
|
56
|
+
priority: options?.priority ?? 0,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
triggers.push(trigger);
|
|
60
|
+
triggers.sort((a, b) => b.priority - a.priority);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
triggers = triggers.filter((t) => t.id !== id);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function removeTrigger(name: string): void {
|
|
68
|
+
triggers = triggers.filter((t) => t.name !== name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fireTrigger(trigger: Trigger): void {
|
|
72
|
+
const { callback, options } = trigger;
|
|
73
|
+
|
|
74
|
+
if (callback) {
|
|
75
|
+
callback();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!options) return;
|
|
80
|
+
|
|
81
|
+
if (options.run) {
|
|
82
|
+
const state = useStoryStore.getState();
|
|
83
|
+
execute(options.run, state.variables, state.temporary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.dialog) {
|
|
87
|
+
dialogQueue.push(options.dialog);
|
|
88
|
+
dialogNotify?.();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (options.goto) {
|
|
92
|
+
useStoryStore.getState().navigate(options.goto);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const MAX_RECHECK_DEPTH = 10;
|
|
97
|
+
|
|
98
|
+
export function checkTriggers(): void {
|
|
99
|
+
if (checking) return;
|
|
100
|
+
checking = true;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
for (let depth = 0; depth < MAX_RECHECK_DEPTH; depth++) {
|
|
104
|
+
let anyFired = false;
|
|
105
|
+
|
|
106
|
+
// Snapshot triggers list — firing may remove `once` triggers
|
|
107
|
+
const current = [...triggers];
|
|
108
|
+
for (const trigger of current) {
|
|
109
|
+
// Skip if removed during this cycle
|
|
110
|
+
if (!triggers.includes(trigger)) continue;
|
|
111
|
+
|
|
112
|
+
const result = evalCondition(trigger.condition);
|
|
113
|
+
const wasFalse = !trigger.lastResult;
|
|
114
|
+
trigger.lastResult = result;
|
|
115
|
+
|
|
116
|
+
if (result && wasFalse) {
|
|
117
|
+
anyFired = true;
|
|
118
|
+
|
|
119
|
+
if (trigger.options?.once) {
|
|
120
|
+
triggers = triggers.filter((t) => t.id !== trigger.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fireTrigger(trigger);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!anyFired) break;
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
checking = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function reinitTriggerState(): void {
|
|
135
|
+
for (const trigger of triggers) {
|
|
136
|
+
trigger.lastResult = evalCondition(trigger.condition);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resetTriggers(): void {
|
|
141
|
+
triggers = [];
|
|
142
|
+
dialogQueue = [];
|
|
143
|
+
nextId = 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function subscribeTriggerDialogs(cb: () => void): () => void {
|
|
147
|
+
dialogNotify = cb;
|
|
148
|
+
// Flush any queued dialogs
|
|
149
|
+
if (dialogQueue.length > 0) cb();
|
|
150
|
+
return () => {
|
|
151
|
+
if (dialogNotify === cb) dialogNotify = null;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function shiftDialogQueue(): string | undefined {
|
|
156
|
+
return dialogQueue.shift();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function dialogQueueLength(): number {
|
|
160
|
+
return dialogQueue.length;
|
|
161
|
+
}
|