yarn-spinner-runner-ts 0.1.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.
Files changed (114) hide show
  1. package/README.md +180 -0
  2. package/dist/compile/compiler.d.ts +9 -0
  3. package/dist/compile/compiler.js +172 -0
  4. package/dist/compile/compiler.js.map +1 -0
  5. package/dist/compile/ir.d.ts +47 -0
  6. package/dist/compile/ir.js +2 -0
  7. package/dist/compile/ir.js.map +1 -0
  8. package/dist/index.d.ts +10 -0
  9. package/dist/index.js +11 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/model/ast.d.ts +72 -0
  12. package/dist/model/ast.js +2 -0
  13. package/dist/model/ast.js.map +1 -0
  14. package/dist/parse/lexer.d.ts +7 -0
  15. package/dist/parse/lexer.js +78 -0
  16. package/dist/parse/lexer.js.map +1 -0
  17. package/dist/parse/parser.d.ts +4 -0
  18. package/dist/parse/parser.js +433 -0
  19. package/dist/parse/parser.js.map +1 -0
  20. package/dist/runtime/commands.d.ts +31 -0
  21. package/dist/runtime/commands.js +157 -0
  22. package/dist/runtime/commands.js.map +1 -0
  23. package/dist/runtime/evaluator.d.ts +52 -0
  24. package/dist/runtime/evaluator.js +309 -0
  25. package/dist/runtime/evaluator.js.map +1 -0
  26. package/dist/runtime/results.d.ts +22 -0
  27. package/dist/runtime/results.js +2 -0
  28. package/dist/runtime/results.js.map +1 -0
  29. package/dist/runtime/runner.d.ts +63 -0
  30. package/dist/runtime/runner.js +456 -0
  31. package/dist/runtime/runner.js.map +1 -0
  32. package/dist/tests/full_featured.test.d.ts +1 -0
  33. package/dist/tests/full_featured.test.js +130 -0
  34. package/dist/tests/full_featured.test.js.map +1 -0
  35. package/dist/tests/index.test.d.ts +1 -0
  36. package/dist/tests/index.test.js +30 -0
  37. package/dist/tests/index.test.js.map +1 -0
  38. package/dist/tests/jump_detour.test.d.ts +1 -0
  39. package/dist/tests/jump_detour.test.js +47 -0
  40. package/dist/tests/jump_detour.test.js.map +1 -0
  41. package/dist/tests/nodes_lines.test.d.ts +1 -0
  42. package/dist/tests/nodes_lines.test.js +23 -0
  43. package/dist/tests/nodes_lines.test.js.map +1 -0
  44. package/dist/tests/once.test.d.ts +1 -0
  45. package/dist/tests/once.test.js +29 -0
  46. package/dist/tests/once.test.js.map +1 -0
  47. package/dist/tests/options.test.d.ts +1 -0
  48. package/dist/tests/options.test.js +32 -0
  49. package/dist/tests/options.test.js.map +1 -0
  50. package/dist/tests/variables_flow_cmds.test.d.ts +1 -0
  51. package/dist/tests/variables_flow_cmds.test.js +30 -0
  52. package/dist/tests/variables_flow_cmds.test.js.map +1 -0
  53. package/dist/types.d.ts +3 -0
  54. package/dist/types.js +2 -0
  55. package/dist/types.js.map +1 -0
  56. package/docs/commands.md +21 -0
  57. package/docs/compatibility-checklist.md +77 -0
  58. package/docs/css-attribute.md +47 -0
  59. package/docs/detour.md +24 -0
  60. package/docs/enums.md +25 -0
  61. package/docs/flow-control.md +25 -0
  62. package/docs/functions.md +20 -0
  63. package/docs/jumps.md +24 -0
  64. package/docs/line-groups.md +21 -0
  65. package/docs/lines-nodes-and-options.md +30 -0
  66. package/docs/logic-and-variables.md +25 -0
  67. package/docs/markup.md +19 -0
  68. package/docs/node-groups.md +13 -0
  69. package/docs/once.md +21 -0
  70. package/docs/options.md +45 -0
  71. package/docs/saliency.md +25 -0
  72. package/docs/scenes-actors-setup.md +195 -0
  73. package/docs/scenes.md +64 -0
  74. package/docs/shadow-lines.md +18 -0
  75. package/docs/smart-variables.md +19 -0
  76. package/docs/storylets-and-saliency-a-primer.md +14 -0
  77. package/docs/tags-metadata.md +18 -0
  78. package/eslint.config.cjs +33 -0
  79. package/examples/browser/README.md +40 -0
  80. package/examples/browser/index.html +23 -0
  81. package/examples/browser/main.tsx +16 -0
  82. package/examples/browser/vite.config.ts +22 -0
  83. package/examples/react/DialogueExample.tsx +2 -0
  84. package/examples/react/DialogueView.tsx +2 -0
  85. package/examples/react/useYarnRunner.tsx +2 -0
  86. package/examples/scenes/scenes.yaml +10 -0
  87. package/examples/yarn/full_featured.yarn +43 -0
  88. package/package.json +55 -0
  89. package/src/compile/compiler.ts +183 -0
  90. package/src/compile/ir.ts +28 -0
  91. package/src/index.ts +17 -0
  92. package/src/model/ast.ts +93 -0
  93. package/src/parse/lexer.ts +108 -0
  94. package/src/parse/parser.ts +435 -0
  95. package/src/react/DialogueExample.tsx +149 -0
  96. package/src/react/DialogueScene.tsx +107 -0
  97. package/src/react/DialogueView.tsx +160 -0
  98. package/src/react/dialogue.css +181 -0
  99. package/src/react/useYarnRunner.tsx +33 -0
  100. package/src/runtime/commands.ts +183 -0
  101. package/src/runtime/evaluator.ts +327 -0
  102. package/src/runtime/results.ts +27 -0
  103. package/src/runtime/runner.ts +480 -0
  104. package/src/scene/parser.ts +83 -0
  105. package/src/scene/types.ts +17 -0
  106. package/src/tests/full_featured.test.ts +131 -0
  107. package/src/tests/index.test.ts +34 -0
  108. package/src/tests/jump_detour.test.ts +47 -0
  109. package/src/tests/nodes_lines.test.ts +27 -0
  110. package/src/tests/once.test.ts +32 -0
  111. package/src/tests/options.test.ts +34 -0
  112. package/src/tests/variables_flow_cmds.test.ts +33 -0
  113. package/src/types.ts +4 -0
  114. package/tsconfig.json +21 -0
@@ -0,0 +1,160 @@
1
+ import React from "react";
2
+ import type { RuntimeResult } from "../runtime/results.js";
3
+ import { DialogueScene } from "./DialogueScene.js";
4
+ import type { SceneCollection } from "../scene/types.js";
5
+ import "./dialogue.css";
6
+
7
+ export interface DialogueViewProps {
8
+ result: RuntimeResult | null;
9
+ onAdvance: (optionIndex?: number) => void;
10
+ className?: string;
11
+ scenes?: SceneCollection;
12
+ }
13
+
14
+ // Helper to parse CSS string into object
15
+ function parseCss(cssStr: string | undefined): React.CSSProperties {
16
+ if (!cssStr) return {};
17
+ const styles: React.CSSProperties = {};
18
+ // Improved parser: handles quoted values and commas
19
+ // Split by semicolon, but preserve quoted strings
20
+ const rules: string[] = [];
21
+ let currentRule = "";
22
+ let inQuotes = false;
23
+ let quoteChar = "";
24
+
25
+ for (let i = 0; i < cssStr.length; i++) {
26
+ const char = cssStr[i];
27
+ if ((char === '"' || char === "'") && !inQuotes) {
28
+ inQuotes = true;
29
+ quoteChar = char;
30
+ currentRule += char;
31
+ } else if (char === quoteChar && inQuotes) {
32
+ inQuotes = false;
33
+ quoteChar = "";
34
+ currentRule += char;
35
+ } else if (char === ";" && !inQuotes) {
36
+ rules.push(currentRule.trim());
37
+ currentRule = "";
38
+ } else {
39
+ currentRule += char;
40
+ }
41
+ }
42
+ if (currentRule.trim()) {
43
+ rules.push(currentRule.trim());
44
+ }
45
+
46
+ rules.forEach((rule) => {
47
+ if (!rule) return;
48
+ const colonIndex = rule.indexOf(":");
49
+ if (colonIndex === -1) return;
50
+ const prop = rule.slice(0, colonIndex).trim();
51
+ const value = rule.slice(colonIndex + 1).trim();
52
+ if (prop && value) {
53
+ // Convert kebab-case to camelCase
54
+ const camelProp = prop.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
55
+ // Remove quotes from value if present, and strip !important (React doesn't support it)
56
+ let cleanValue = value.trim();
57
+ if (cleanValue.endsWith("!important")) {
58
+ cleanValue = cleanValue.slice(0, -10).trim();
59
+ }
60
+ if (cleanValue.startsWith('"') && cleanValue.endsWith('"')) {
61
+ cleanValue = cleanValue.slice(1, -1);
62
+ } else if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
63
+ cleanValue = cleanValue.slice(1, -1);
64
+ }
65
+ (styles as any)[camelProp] = cleanValue;
66
+ }
67
+ });
68
+ return styles;
69
+ }
70
+
71
+ export function DialogueView({ result, onAdvance, className, scenes }: DialogueViewProps) {
72
+ const sceneName = result?.type === "text" || result?.type === "options" ? result.scene : undefined;
73
+ const speaker = result?.type === "text" ? result.speaker : undefined;
74
+ const sceneCollection = scenes || { scenes: {} };
75
+
76
+ if (!result) {
77
+ return (
78
+ <div className={`yd-empty ${className || ""}`}>
79
+ <p>Dialogue ended or not started.</p>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ if (result.type === "text") {
85
+ const nodeStyles = parseCss(result.nodeCss);
86
+ return (
87
+ <div className="yd-container">
88
+ <DialogueScene sceneName={sceneName} speaker={speaker} scenes={sceneCollection} />
89
+ <div
90
+ className={`yd-dialogue-box ${result.isDialogueEnd ? "yd-text-box-end" : ""} ${className || ""}`}
91
+ style={nodeStyles} // Only apply dynamic node CSS
92
+ onClick={() => !result.isDialogueEnd && onAdvance()}
93
+ >
94
+ <div className="yd-text-box">
95
+ {result.speaker && (
96
+ <div className="yd-speaker">
97
+ {result.speaker}
98
+ </div>
99
+ )}
100
+ <p className={`yd-text ${result.speaker ? "yd-text-with-speaker" : ""}`}>
101
+ {result.text || "\u00A0"}
102
+ </p>
103
+ {!result.isDialogueEnd && (
104
+ <div className="yd-continue">
105
+
106
+ </div>
107
+ )}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ if (result.type === "options") {
115
+ const nodeStyles = parseCss(result.nodeCss);
116
+ return (
117
+ <div className="yd-container">
118
+ <DialogueScene sceneName={sceneName} speaker={speaker} scenes={sceneCollection} />
119
+ <div className={`yd-options-container ${className || ""}`}>
120
+ <div className="yd-options-box" style={nodeStyles}>
121
+ <div className="yd-options-title">Choose an option:</div>
122
+ <div className="yd-options-list">
123
+ {result.options.map((option, index) => {
124
+ const optionStyles = parseCss(option.css);
125
+ return (
126
+ <button
127
+ key={index}
128
+ className="yd-option-button"
129
+ onClick={() => onAdvance(index)}
130
+ style={optionStyles} // Only apply dynamic option CSS
131
+ >
132
+ {option.text}
133
+ </button>
134
+ );
135
+ })}
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ // Command result - auto-advance
144
+ if (result.type === "command") {
145
+ // Auto-advance commands after a brief moment
146
+ React.useEffect(() => {
147
+ const timer = setTimeout(() => onAdvance(), 50);
148
+ return () => clearTimeout(timer);
149
+ }, [result.command, onAdvance]);
150
+
151
+ return (
152
+ <div className={`yd-command ${className || ""}`}>
153
+ <p>Executing: {result.command}</p>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ return null;
159
+ }
160
+
@@ -0,0 +1,181 @@
1
+ /* Yarn Dialogue System Styles */
2
+
3
+ /* Main dialogue container */
4
+ .yd-container {
5
+ position: relative;
6
+ width: 100%;
7
+ min-height: 400px;
8
+ display: flex;
9
+ flex-direction: column;
10
+ }
11
+
12
+ /* Scene container */
13
+ .yd-scene {
14
+ position: absolute;
15
+ top: 0;
16
+ left: 0;
17
+ right: 0;
18
+ bottom: 0;
19
+ width: 100%;
20
+ height: 100%;
21
+ min-height: 400px;
22
+ background-color: rgba(26, 26, 46, 1);
23
+ background-size: cover;
24
+ background-position: center;
25
+ background-repeat: no-repeat;
26
+ transition: opacity 0.6s ease-in-out;
27
+ z-index: 0;
28
+ overflow: visible;
29
+ pointer-events: none;
30
+ }
31
+
32
+ /* Scene background transition overlay */
33
+ .yd-scene-next {
34
+ position: absolute;
35
+ top: 0;
36
+ left: 0;
37
+ right: 0;
38
+ bottom: 0;
39
+ background-size: cover;
40
+ background-position: center;
41
+ background-repeat: no-repeat;
42
+ z-index: 0;
43
+ }
44
+
45
+ /* Actor image */
46
+ .yd-actor {
47
+ position: absolute;
48
+ top: 0;
49
+ left: 50%;
50
+ transform: translateX(-50%);
51
+ max-height: 70%;
52
+ max-width: 40%;
53
+ object-fit: contain;
54
+ z-index: 1;
55
+ transition: opacity 0.3s ease-in-out;
56
+ opacity: 1;
57
+ pointer-events: none;
58
+ }
59
+
60
+ /* Dialogue box container */
61
+ .yd-dialogue-box {
62
+ position: absolute;
63
+ bottom: 0;
64
+ left: 0;
65
+ right: 0;
66
+ z-index: 10;
67
+ margin: 20px;
68
+ width: calc(100% - 40px);
69
+ cursor: pointer;
70
+ }
71
+
72
+ .yd-dialogue-box.yd-text-box-end {
73
+ cursor: default;
74
+ }
75
+
76
+ /* Text dialogue box content */
77
+ .yd-text-box {
78
+ background-color: rgba(30, 30, 60, 0.9);
79
+ border: 2px solid #4a9eff;
80
+ border-radius: 12px;
81
+ padding: 20px;
82
+ color: #ffffff;
83
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
84
+ min-height: 120px;
85
+ }
86
+
87
+ /* Speaker label */
88
+ .yd-speaker {
89
+ background-color: #4a9eff;
90
+ color: #ffffff;
91
+ padding: 8px 16px;
92
+ border-radius: 8px;
93
+ display: inline-block;
94
+ margin-bottom: 12px;
95
+ font-size: 14px;
96
+ font-weight: bold;
97
+ }
98
+
99
+ /* Dialogue text */
100
+ .yd-text {
101
+ margin: 0;
102
+ font-size: 18px;
103
+ line-height: 1.6;
104
+ }
105
+
106
+ .yd-text-with-speaker {
107
+ margin-top: 12px;
108
+ }
109
+
110
+ /* Continue indicator */
111
+ .yd-continue {
112
+ text-align: right;
113
+ margin-top: 12px;
114
+ font-size: 24px;
115
+ color: #4a9eff;
116
+ }
117
+
118
+ /* Options container */
119
+ .yd-options-container {
120
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
121
+ position: absolute;
122
+ bottom: 0;
123
+ left: 0;
124
+ right: 0;
125
+ z-index: 10;
126
+ margin: 20px;
127
+ width: calc(100% - 40px);
128
+ }
129
+
130
+ .yd-options-box {
131
+ background-color: rgba(30, 30, 60, 0.9);
132
+ border: 2px solid #4a9eff;
133
+ border-radius: 12px;
134
+ padding: 20px;
135
+ color: #ffffff;
136
+ }
137
+
138
+ .yd-options-title {
139
+ margin-bottom: 16px;
140
+ font-size: 16px;
141
+ color: #a0a0ff;
142
+ }
143
+
144
+ .yd-options-list {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: 12px;
148
+ }
149
+
150
+ /* Option button */
151
+ .yd-option-button {
152
+ border: none;
153
+ border-radius: 8px;
154
+ padding: 16px 20px;
155
+ font-size: 16px;
156
+ cursor: pointer;
157
+ text-align: left;
158
+ transition: transform 0.1s, opacity 0.1s;
159
+ font-family: inherit;
160
+ background-color: #4a9eff;
161
+ color: #ffffff;
162
+ }
163
+
164
+ .yd-option-button:hover {
165
+ transform: scale(1.02);
166
+ opacity: 0.9;
167
+ }
168
+
169
+ /* Command result */
170
+ .yd-command {
171
+ padding: 20px;
172
+ text-align: center;
173
+ color: #888;
174
+ }
175
+
176
+ /* Empty/ended dialogue */
177
+ .yd-empty {
178
+ padding: 20px;
179
+ text-align: center;
180
+ }
181
+
@@ -0,0 +1,33 @@
1
+ import { useState, useCallback, useRef } from "react";
2
+ import { YarnRunner, type IRProgram, type RunnerOptions, type RuntimeResult } from "../runtime/runner.js";
3
+
4
+ export function useYarnRunner(
5
+ program: IRProgram,
6
+ options: RunnerOptions
7
+ ): {
8
+ result: RuntimeResult | null;
9
+ advance: (optionIndex?: number) => void;
10
+ runner: YarnRunner;
11
+ } {
12
+ const runnerRef = useRef<YarnRunner | null>(null);
13
+ const [result, setResult] = useState<RuntimeResult | null>(null);
14
+
15
+ // Initialize runner only once
16
+ if (!runnerRef.current) {
17
+ runnerRef.current = new YarnRunner(program, options);
18
+ setResult(runnerRef.current.currentResult);
19
+ }
20
+
21
+ const runner = runnerRef.current;
22
+
23
+ const advance = useCallback(
24
+ (optionIndex?: number) => {
25
+ runner.advance(optionIndex);
26
+ setResult(runner.currentResult);
27
+ },
28
+ [runner]
29
+ );
30
+
31
+ return { result, advance, runner };
32
+ }
33
+
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Command parser and handler utilities for Yarn Spinner commands.
3
+ * Commands like <<command_name arg1 arg2>> or <<command_name "arg with spaces">>
4
+ */
5
+
6
+ import type { ExpressionEvaluator as Evaluator } from "./evaluator";
7
+
8
+ export interface ParsedCommand {
9
+ name: string;
10
+ args: string[];
11
+ raw: string;
12
+ }
13
+
14
+ /**
15
+ * Parse a command string like "command_name arg1 arg2" or "set variable value"
16
+ */
17
+ export function parseCommand(content: string): ParsedCommand {
18
+ const trimmed = content.trim();
19
+ if (!trimmed) {
20
+ throw new Error("Empty command");
21
+ }
22
+
23
+ const parts: string[] = [];
24
+ let current = "";
25
+ let inQuotes = false;
26
+ let quoteChar = "";
27
+
28
+ for (let i = 0; i < trimmed.length; i++) {
29
+ const char = trimmed[i];
30
+
31
+ if ((char === '"' || char === "'") && !inQuotes) {
32
+ inQuotes = true;
33
+ quoteChar = char;
34
+ continue;
35
+ }
36
+
37
+ if (char === quoteChar && inQuotes) {
38
+ inQuotes = false;
39
+ quoteChar = "";
40
+ parts.push(current);
41
+ current = "";
42
+ continue;
43
+ }
44
+
45
+ if (char === " " && !inQuotes) {
46
+ if (current.trim()) {
47
+ parts.push(current.trim());
48
+ current = "";
49
+ }
50
+ continue;
51
+ }
52
+
53
+ current += char;
54
+ }
55
+
56
+ if (current.trim()) {
57
+ parts.push(current.trim());
58
+ }
59
+
60
+ if (parts.length === 0) {
61
+ throw new Error("No command name found");
62
+ }
63
+
64
+ return {
65
+ name: parts[0],
66
+ args: parts.slice(1),
67
+ raw: content,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Built-in command handlers for common Yarn Spinner commands.
73
+ */
74
+ export class CommandHandler {
75
+ private handlers = new Map<string, (args: string[], evaluator?: Evaluator) => void | Promise<void>>();
76
+ private variables: Record<string, unknown>;
77
+
78
+ constructor(variables: Record<string, unknown> = {}) {
79
+ this.variables = variables;
80
+ this.registerBuiltins();
81
+ }
82
+
83
+ /**
84
+ * Register a command handler.
85
+ */
86
+ register(name: string, handler: (args: string[], evaluator?: Evaluator) => void | Promise<void>): void {
87
+ this.handlers.set(name.toLowerCase(), handler);
88
+ }
89
+
90
+ /**
91
+ * Execute a parsed command.
92
+ */
93
+ async execute(parsed: ParsedCommand, evaluator?: Evaluator): Promise<void> {
94
+ const handler = this.handlers.get(parsed.name.toLowerCase());
95
+ if (handler) {
96
+ await handler(parsed.args, evaluator);
97
+ } else {
98
+ console.warn(`Unknown command: ${parsed.name}`);
99
+ }
100
+ }
101
+
102
+ private registerBuiltins(): void {
103
+ // <<set $var to expr>> or <<set $var = expr>> or <<set $var expr>>
104
+ this.register("set", (args, evaluator) => {
105
+ if (!evaluator) return;
106
+ if (args.length < 2) return;
107
+ const varNameRaw = args[0];
108
+ let exprParts = args.slice(1);
109
+ if (exprParts[0] === "to") exprParts = exprParts.slice(1);
110
+ if (exprParts[0] === "=") exprParts = exprParts.slice(1);
111
+ const expr = exprParts.join(" ");
112
+ let value = evaluator.evaluateExpression(expr);
113
+
114
+ // If value is a string starting with ".", try to resolve as enum shorthand
115
+ if (typeof value === "string" && value.startsWith(".")) {
116
+ const enumType = evaluator.getEnumTypeForVariable(varNameRaw);
117
+ if (enumType) {
118
+ value = evaluator.resolveEnumValue(value, enumType);
119
+ }
120
+ }
121
+
122
+ const key = varNameRaw.startsWith("$") ? varNameRaw.slice(1) : varNameRaw;
123
+ // Setting a variable converts it from smart to regular
124
+ this.variables[key] = value;
125
+ evaluator.setVariable(key, value);
126
+ });
127
+
128
+ // <<declare $var = expr>>
129
+ this.register("declare", (args, evaluator) => {
130
+ if (!evaluator) return;
131
+ if (args.length < 3) return; // name, '=', expr
132
+ const varNameRaw = args[0];
133
+ let exprParts = args.slice(1);
134
+ if (exprParts[0] === "=") exprParts = exprParts.slice(1);
135
+ const expr = exprParts.join(" ");
136
+
137
+ const key = varNameRaw.startsWith("$") ? varNameRaw.slice(1) : varNameRaw;
138
+
139
+ // Check if expression is "smart" (contains operators, comparisons, or variable references)
140
+ // Smart variables: expressions with operators, comparisons, logical ops, or function calls
141
+ const isSmart = /[+\-*/%<>=!&|]/.test(expr) ||
142
+ /\$\w+/.test(expr) || // references other variables
143
+ /[a-zA-Z_]\w*\s*\(/.test(expr); // function calls
144
+
145
+ if (isSmart) {
146
+ // Store as smart variable - will recalculate on each access
147
+ evaluator.setSmartVariable(key, expr);
148
+ // Also store initial value in variables for immediate use
149
+ const initialValue = evaluator.evaluateExpression(expr);
150
+ this.variables[key] = initialValue;
151
+ } else {
152
+ // Regular variable - evaluate once and store
153
+ let value = evaluator.evaluateExpression(expr);
154
+
155
+ // Check if expr is an enum value (EnumName.CaseName or .CaseName)
156
+ if (typeof value === "string") {
157
+ // Try to extract enum name from EnumName.CaseName
158
+ const enumMatch = expr.match(/^([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/);
159
+ if (enumMatch) {
160
+ const enumName = enumMatch[1];
161
+ value = evaluator.resolveEnumValue(expr, enumName);
162
+ } else if (value.startsWith(".")) {
163
+ // Shorthand - we can't infer enum type from declaration alone
164
+ // Store as-is, will be resolved on first use if variable has enum type
165
+ value = value;
166
+ }
167
+ }
168
+
169
+ this.variables[key] = value;
170
+ evaluator.setVariable(key, value);
171
+ }
172
+ });
173
+
174
+ // <<stop>> - no-op, just a marker
175
+ this.register("stop", () => {
176
+ // Dialogue stop marker
177
+ });
178
+ }
179
+ }
180
+
181
+ // Forward reference type (avoid circular import)
182
+ type ExpressionEvaluator = import("./evaluator").ExpressionEvaluator;
183
+