@magicborn/dialogue-forge 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 (241) hide show
  1. package/README.md +233 -0
  2. package/bin/dialogue-forge.js +78 -0
  3. package/demo/app/layout.tsx +36 -0
  4. package/demo/app/page.tsx +440 -0
  5. package/demo/components/ThemeSwitcher.tsx +611 -0
  6. package/demo/next.config.mjs +7 -0
  7. package/demo/package.json +29 -0
  8. package/demo/postcss.config.mjs +7 -0
  9. package/demo/public/logo.svg +1 -0
  10. package/demo/styles/globals.css +19 -0
  11. package/demo/tailwind.config.ts +90 -0
  12. package/demo/tsconfig.json +42 -0
  13. package/dist/components/ChoiceEdgeV2.d.ts +3 -0
  14. package/dist/components/ChoiceEdgeV2.js +103 -0
  15. package/dist/components/CodeBlock.d.ts +8 -0
  16. package/dist/components/CodeBlock.js +24 -0
  17. package/dist/components/ConditionAutocomplete.d.ts +14 -0
  18. package/dist/components/ConditionAutocomplete.js +284 -0
  19. package/dist/components/ConditionalNodeV2.d.ts +16 -0
  20. package/dist/components/ConditionalNodeV2.js +147 -0
  21. package/dist/components/DialogueEditorV2.d.ts +22 -0
  22. package/dist/components/DialogueEditorV2.js +1170 -0
  23. package/dist/components/EdgeIcon.d.ts +8 -0
  24. package/dist/components/EdgeIcon.js +13 -0
  25. package/dist/components/ExampleLoader.d.ts +11 -0
  26. package/dist/components/ExampleLoader.js +52 -0
  27. package/dist/components/ExampleLoaderButton.d.ts +15 -0
  28. package/dist/components/ExampleLoaderButton.js +102 -0
  29. package/dist/components/FlagManager.d.ts +11 -0
  30. package/dist/components/FlagManager.js +282 -0
  31. package/dist/components/FlagSelector.d.ts +11 -0
  32. package/dist/components/FlagSelector.js +235 -0
  33. package/dist/components/GuidePanel.d.ts +7 -0
  34. package/dist/components/GuidePanel.js +1176 -0
  35. package/dist/components/Minimap.d.ts +16 -0
  36. package/dist/components/Minimap.js +93 -0
  37. package/dist/components/NPCEdgeV2.d.ts +3 -0
  38. package/dist/components/NPCEdgeV2.js +104 -0
  39. package/dist/components/NPCNodeV2.d.ts +26 -0
  40. package/dist/components/NPCNodeV2.js +86 -0
  41. package/dist/components/NodeEditor.d.ts +18 -0
  42. package/dist/components/NodeEditor.js +1025 -0
  43. package/dist/components/PlayView.d.ts +12 -0
  44. package/dist/components/PlayView.js +307 -0
  45. package/dist/components/PlayerNodeV2.d.ts +16 -0
  46. package/dist/components/PlayerNodeV2.js +139 -0
  47. package/dist/components/ReactFlowPOC.d.ts +61 -0
  48. package/dist/components/ReactFlowPOC.js +312 -0
  49. package/dist/components/ScenePlayer.d.ts +18 -0
  50. package/dist/components/ScenePlayer.js +196 -0
  51. package/dist/components/YarnView.d.ts +9 -0
  52. package/dist/components/YarnView.js +45 -0
  53. package/dist/components/ZoomControls.d.ts +11 -0
  54. package/dist/components/ZoomControls.js +34 -0
  55. package/dist/esm/components/ChoiceEdgeV2.d.ts +3 -0
  56. package/dist/esm/components/ChoiceEdgeV2.js +67 -0
  57. package/dist/esm/components/CodeBlock.d.ts +8 -0
  58. package/dist/esm/components/CodeBlock.js +18 -0
  59. package/dist/esm/components/ConditionAutocomplete.d.ts +14 -0
  60. package/dist/esm/components/ConditionAutocomplete.js +248 -0
  61. package/dist/esm/components/ConditionalNodeV2.d.ts +16 -0
  62. package/dist/esm/components/ConditionalNodeV2.js +111 -0
  63. package/dist/esm/components/DialogueEditorV2.d.ts +22 -0
  64. package/dist/esm/components/DialogueEditorV2.js +1134 -0
  65. package/dist/esm/components/EdgeIcon.d.ts +8 -0
  66. package/dist/esm/components/EdgeIcon.js +7 -0
  67. package/dist/esm/components/ExampleLoader.d.ts +11 -0
  68. package/dist/esm/components/ExampleLoader.js +46 -0
  69. package/dist/esm/components/ExampleLoaderButton.d.ts +15 -0
  70. package/dist/esm/components/ExampleLoaderButton.js +66 -0
  71. package/dist/esm/components/FlagManager.d.ts +11 -0
  72. package/dist/esm/components/FlagManager.js +246 -0
  73. package/dist/esm/components/FlagSelector.d.ts +11 -0
  74. package/dist/esm/components/FlagSelector.js +199 -0
  75. package/dist/esm/components/GuidePanel.d.ts +7 -0
  76. package/dist/esm/components/GuidePanel.js +1140 -0
  77. package/dist/esm/components/Minimap.d.ts +16 -0
  78. package/dist/esm/components/Minimap.js +57 -0
  79. package/dist/esm/components/NPCEdgeV2.d.ts +3 -0
  80. package/dist/esm/components/NPCEdgeV2.js +68 -0
  81. package/dist/esm/components/NPCNodeV2.d.ts +26 -0
  82. package/dist/esm/components/NPCNodeV2.js +80 -0
  83. package/dist/esm/components/NodeEditor.d.ts +18 -0
  84. package/dist/esm/components/NodeEditor.js +989 -0
  85. package/dist/esm/components/PlayView.d.ts +12 -0
  86. package/dist/esm/components/PlayView.js +271 -0
  87. package/dist/esm/components/PlayerNodeV2.d.ts +16 -0
  88. package/dist/esm/components/PlayerNodeV2.js +103 -0
  89. package/dist/esm/components/ReactFlowPOC.d.ts +61 -0
  90. package/dist/esm/components/ReactFlowPOC.js +275 -0
  91. package/dist/esm/components/ScenePlayer.d.ts +18 -0
  92. package/dist/esm/components/ScenePlayer.js +160 -0
  93. package/dist/esm/components/YarnView.d.ts +9 -0
  94. package/dist/esm/components/YarnView.js +39 -0
  95. package/dist/esm/components/ZoomControls.d.ts +11 -0
  96. package/dist/esm/components/ZoomControls.js +28 -0
  97. package/dist/esm/examples/example-loader.d.ts +29 -0
  98. package/dist/esm/examples/example-loader.js +103 -0
  99. package/dist/esm/examples/examples-registry.d.ts +38 -0
  100. package/dist/esm/examples/examples-registry.js +153 -0
  101. package/dist/esm/examples/index.d.ts +26 -0
  102. package/dist/esm/examples/index.js +50 -0
  103. package/dist/esm/examples/legacy-examples.d.ts +9 -0
  104. package/dist/esm/examples/legacy-examples.js +814 -0
  105. package/dist/esm/examples/yarn-examples.d.ts +35 -0
  106. package/dist/esm/examples/yarn-examples.js +181 -0
  107. package/dist/esm/index.d.ts +21 -0
  108. package/dist/esm/index.js +26 -0
  109. package/dist/esm/lib/flag-manager.d.ts +21 -0
  110. package/dist/esm/lib/flag-manager.js +93 -0
  111. package/dist/esm/lib/yarn-converter/__tests__/round-trip.test.d.ts +1 -0
  112. package/dist/esm/lib/yarn-converter/__tests__/round-trip.test.js +169 -0
  113. package/dist/esm/lib/yarn-converter.d.ts +17 -0
  114. package/dist/esm/lib/yarn-converter.js +521 -0
  115. package/dist/esm/lib/yarn-runner/__tests__/condition-evaluator.test.d.ts +1 -0
  116. package/dist/esm/lib/yarn-runner/__tests__/condition-evaluator.test.js +171 -0
  117. package/dist/esm/lib/yarn-runner/__tests__/node-processor.test.d.ts +1 -0
  118. package/dist/esm/lib/yarn-runner/__tests__/node-processor.test.js +237 -0
  119. package/dist/esm/lib/yarn-runner/__tests__/variable-manager.test.d.ts +1 -0
  120. package/dist/esm/lib/yarn-runner/__tests__/variable-manager.test.js +106 -0
  121. package/dist/esm/lib/yarn-runner/condition-evaluator.d.ts +12 -0
  122. package/dist/esm/lib/yarn-runner/condition-evaluator.js +56 -0
  123. package/dist/esm/lib/yarn-runner/index.d.ts +12 -0
  124. package/dist/esm/lib/yarn-runner/index.js +11 -0
  125. package/dist/esm/lib/yarn-runner/node-processor.d.ts +18 -0
  126. package/dist/esm/lib/yarn-runner/node-processor.js +129 -0
  127. package/dist/esm/lib/yarn-runner/variable-manager.d.ts +51 -0
  128. package/dist/esm/lib/yarn-runner/variable-manager.js +120 -0
  129. package/dist/esm/lib/yarn-runner/variable-operations.d.ts +16 -0
  130. package/dist/esm/lib/yarn-runner/variable-operations.js +88 -0
  131. package/dist/esm/types/conditionals.d.ts +29 -0
  132. package/dist/esm/types/conditionals.js +1 -0
  133. package/dist/esm/types/constants.d.ts +59 -0
  134. package/dist/esm/types/constants.js +55 -0
  135. package/dist/esm/types/flags.d.ts +49 -0
  136. package/dist/esm/types/flags.js +49 -0
  137. package/dist/esm/types/game-state.d.ts +62 -0
  138. package/dist/esm/types/game-state.js +6 -0
  139. package/dist/esm/types/index.d.ts +77 -0
  140. package/dist/esm/types/index.js +1 -0
  141. package/dist/esm/utils/constants.d.ts +5 -0
  142. package/dist/esm/utils/constants.js +5 -0
  143. package/dist/esm/utils/feature-flags.d.ts +11 -0
  144. package/dist/esm/utils/feature-flags.js +11 -0
  145. package/dist/esm/utils/game-state-flattener.d.ts +41 -0
  146. package/dist/esm/utils/game-state-flattener.js +135 -0
  147. package/dist/esm/utils/layout/collision.d.ts +27 -0
  148. package/dist/esm/utils/layout/collision.js +74 -0
  149. package/dist/esm/utils/layout/index.d.ts +82 -0
  150. package/dist/esm/utils/layout/index.js +98 -0
  151. package/dist/esm/utils/layout/registry.d.ts +91 -0
  152. package/dist/esm/utils/layout/registry.js +148 -0
  153. package/dist/esm/utils/layout/strategies/dagre.d.ts +19 -0
  154. package/dist/esm/utils/layout/strategies/dagre.js +182 -0
  155. package/dist/esm/utils/layout/strategies/force.d.ts +21 -0
  156. package/dist/esm/utils/layout/strategies/force.js +178 -0
  157. package/dist/esm/utils/layout/strategies/grid.d.ts +17 -0
  158. package/dist/esm/utils/layout/strategies/grid.js +91 -0
  159. package/dist/esm/utils/layout/strategies/index.d.ts +8 -0
  160. package/dist/esm/utils/layout/strategies/index.js +8 -0
  161. package/dist/esm/utils/layout/types.d.ts +100 -0
  162. package/dist/esm/utils/layout/types.js +7 -0
  163. package/dist/esm/utils/layout.d.ts +9 -0
  164. package/dist/esm/utils/layout.js +17 -0
  165. package/dist/esm/utils/node-helpers.d.ts +7 -0
  166. package/dist/esm/utils/node-helpers.js +94 -0
  167. package/dist/esm/utils/reactflow-converter.d.ts +42 -0
  168. package/dist/esm/utils/reactflow-converter.js +217 -0
  169. package/dist/examples/example-loader.d.ts +29 -0
  170. package/dist/examples/example-loader.js +109 -0
  171. package/dist/examples/examples-registry.d.ts +38 -0
  172. package/dist/examples/examples-registry.js +160 -0
  173. package/dist/examples/index.d.ts +26 -0
  174. package/dist/examples/index.js +63 -0
  175. package/dist/examples/legacy-examples.d.ts +9 -0
  176. package/dist/examples/legacy-examples.js +817 -0
  177. package/dist/examples/yarn-examples.d.ts +35 -0
  178. package/dist/examples/yarn-examples.js +189 -0
  179. package/dist/index.d.ts +21 -0
  180. package/dist/index.js +66 -0
  181. package/dist/lib/flag-manager.d.ts +21 -0
  182. package/dist/lib/flag-manager.js +99 -0
  183. package/dist/lib/yarn-converter/__tests__/round-trip.test.d.ts +1 -0
  184. package/dist/lib/yarn-converter/__tests__/round-trip.test.js +171 -0
  185. package/dist/lib/yarn-converter.d.ts +17 -0
  186. package/dist/lib/yarn-converter.js +525 -0
  187. package/dist/lib/yarn-runner/__tests__/condition-evaluator.test.d.ts +1 -0
  188. package/dist/lib/yarn-runner/__tests__/condition-evaluator.test.js +173 -0
  189. package/dist/lib/yarn-runner/__tests__/node-processor.test.d.ts +1 -0
  190. package/dist/lib/yarn-runner/__tests__/node-processor.test.js +239 -0
  191. package/dist/lib/yarn-runner/__tests__/variable-manager.test.d.ts +1 -0
  192. package/dist/lib/yarn-runner/__tests__/variable-manager.test.js +108 -0
  193. package/dist/lib/yarn-runner/condition-evaluator.d.ts +12 -0
  194. package/dist/lib/yarn-runner/condition-evaluator.js +60 -0
  195. package/dist/lib/yarn-runner/index.d.ts +12 -0
  196. package/dist/lib/yarn-runner/index.js +21 -0
  197. package/dist/lib/yarn-runner/node-processor.d.ts +18 -0
  198. package/dist/lib/yarn-runner/node-processor.js +133 -0
  199. package/dist/lib/yarn-runner/variable-manager.d.ts +51 -0
  200. package/dist/lib/yarn-runner/variable-manager.js +124 -0
  201. package/dist/lib/yarn-runner/variable-operations.d.ts +16 -0
  202. package/dist/lib/yarn-runner/variable-operations.js +92 -0
  203. package/dist/types/conditionals.d.ts +29 -0
  204. package/dist/types/conditionals.js +2 -0
  205. package/dist/types/constants.d.ts +59 -0
  206. package/dist/types/constants.js +58 -0
  207. package/dist/types/flags.d.ts +49 -0
  208. package/dist/types/flags.js +52 -0
  209. package/dist/types/game-state.d.ts +62 -0
  210. package/dist/types/game-state.js +7 -0
  211. package/dist/types/index.d.ts +77 -0
  212. package/dist/types/index.js +2 -0
  213. package/dist/utils/constants.d.ts +5 -0
  214. package/dist/utils/constants.js +8 -0
  215. package/dist/utils/feature-flags.d.ts +11 -0
  216. package/dist/utils/feature-flags.js +14 -0
  217. package/dist/utils/game-state-flattener.d.ts +41 -0
  218. package/dist/utils/game-state-flattener.js +140 -0
  219. package/dist/utils/layout/collision.d.ts +27 -0
  220. package/dist/utils/layout/collision.js +77 -0
  221. package/dist/utils/layout/index.d.ts +82 -0
  222. package/dist/utils/layout/index.js +109 -0
  223. package/dist/utils/layout/registry.d.ts +91 -0
  224. package/dist/utils/layout/registry.js +151 -0
  225. package/dist/utils/layout/strategies/dagre.d.ts +19 -0
  226. package/dist/utils/layout/strategies/dagre.js +189 -0
  227. package/dist/utils/layout/strategies/force.d.ts +21 -0
  228. package/dist/utils/layout/strategies/force.js +182 -0
  229. package/dist/utils/layout/strategies/grid.d.ts +17 -0
  230. package/dist/utils/layout/strategies/grid.js +95 -0
  231. package/dist/utils/layout/strategies/index.d.ts +8 -0
  232. package/dist/utils/layout/strategies/index.js +14 -0
  233. package/dist/utils/layout/types.d.ts +100 -0
  234. package/dist/utils/layout/types.js +8 -0
  235. package/dist/utils/layout.d.ts +9 -0
  236. package/dist/utils/layout.js +25 -0
  237. package/dist/utils/node-helpers.d.ts +7 -0
  238. package/dist/utils/node-helpers.js +101 -0
  239. package/dist/utils/reactflow-converter.d.ts +42 -0
  240. package/dist/utils/reactflow-converter.js +223 -0
  241. package/package.json +70 -0
@@ -0,0 +1,521 @@
1
+ /**
2
+ * Convert DialogueTree to Yarn Spinner format
3
+ *
4
+ * Flags are converted to Yarn variables ($variable).
5
+ * Variables are NOT stored in the .yarn file - they're managed by
6
+ * Yarn Spinner's Variable Storage at runtime.
7
+ *
8
+ * The .yarn file contains commands like:
9
+ * - <<set $flag_name = value>> - Sets variable in Variable Storage
10
+ * - <<if $flag_name>> - Checks variable in Variable Storage
11
+ */
12
+ export function exportToYarn(tree) {
13
+ let yarn = '';
14
+ Object.values(tree.nodes).forEach(node => {
15
+ yarn += `title: ${node.id}\n`;
16
+ yarn += `---\n`;
17
+ if (node.type === 'npc') {
18
+ // Export conditional blocks if present
19
+ if (node.conditionalBlocks && node.conditionalBlocks.length > 0) {
20
+ node.conditionalBlocks.forEach(block => {
21
+ if (block.type === 'if' || block.type === 'elseif') {
22
+ // Build condition string
23
+ const conditions = block.condition?.map(cond => {
24
+ const varName = `$${cond.flag}`;
25
+ if (cond.operator === 'is_set') {
26
+ return varName;
27
+ }
28
+ else if (cond.operator === 'is_not_set') {
29
+ return `not ${varName}`;
30
+ }
31
+ else if (cond.value !== undefined) {
32
+ const op = cond.operator === 'equals' ? '==' :
33
+ cond.operator === 'not_equals' ? '!=' :
34
+ cond.operator === 'greater_than' ? '>' :
35
+ cond.operator === 'less_than' ? '<' :
36
+ cond.operator === 'greater_equal' ? '>=' :
37
+ cond.operator === 'less_equal' ? '<=' : '==';
38
+ const value = typeof cond.value === 'string' ? `"${cond.value}"` : cond.value;
39
+ return `${varName} ${op} ${value}`;
40
+ }
41
+ return '';
42
+ }).filter(c => c).join(' and ') || '';
43
+ yarn += `<<${block.type} ${conditions}>>\n`;
44
+ }
45
+ else if (block.type === 'else') {
46
+ yarn += `<<else>>\n`;
47
+ }
48
+ // Export block content (remove set commands from content, they'll be exported separately)
49
+ let blockContent = block.content;
50
+ const setCommandsInBlock = blockContent.match(/<<set\s+\$(\w+)\s*([+\-*/=]+)\s*(.+?)>>/g);
51
+ if (setCommandsInBlock) {
52
+ // Remove set commands from content
53
+ setCommandsInBlock.forEach(cmd => {
54
+ blockContent = blockContent.replace(cmd, '').trim();
55
+ });
56
+ }
57
+ if (block.speaker) {
58
+ yarn += `${block.speaker}: ${blockContent.replace(/\n/g, '\n' + block.speaker + ': ')}\n`;
59
+ }
60
+ else {
61
+ yarn += `${blockContent}\n`;
62
+ }
63
+ // Export variable operations from this block
64
+ if (setCommandsInBlock) {
65
+ setCommandsInBlock.forEach(cmd => {
66
+ yarn += `${cmd}\n`;
67
+ });
68
+ }
69
+ });
70
+ yarn += `<<endif>>\n`;
71
+ }
72
+ else {
73
+ // Regular content (no conditionals)
74
+ let content = node.content;
75
+ const setCommandsInContent = content.match(/<<set\s+\$(\w+)\s*([+\-*/=]+)\s*(.+?)>>/g);
76
+ if (setCommandsInContent) {
77
+ // Remove set commands from content
78
+ setCommandsInContent.forEach(cmd => {
79
+ content = content.replace(cmd, '').trim();
80
+ });
81
+ }
82
+ if (node.speaker) {
83
+ yarn += `${node.speaker}: ${content.replace(/\n/g, '\n' + node.speaker + ': ')}\n`;
84
+ }
85
+ else {
86
+ yarn += `${content}\n`;
87
+ }
88
+ }
89
+ // Export flags as Yarn variable commands
90
+ // Check if content contains variable operations first
91
+ const setCommandsInContent = node.content.match(/<<set\s+\$(\w+)\s*([+\-*/=]+)\s*(.+?)>>/g);
92
+ if (setCommandsInContent) {
93
+ // Export the operations found in content
94
+ setCommandsInContent.forEach(cmd => {
95
+ yarn += `${cmd}\n`;
96
+ });
97
+ }
98
+ else if (node.setFlags?.length) {
99
+ // Fallback: export as boolean flags
100
+ node.setFlags.forEach(flag => {
101
+ yarn += `<<set $${flag} = true>>\n`;
102
+ });
103
+ }
104
+ if (node.nextNodeId) {
105
+ yarn += `<<jump ${node.nextNodeId}>>\n`;
106
+ }
107
+ }
108
+ else if (node.type === 'conditional' && node.conditionalBlocks) {
109
+ // Export conditional node blocks
110
+ node.conditionalBlocks.forEach(block => {
111
+ if (block.type === 'if' || block.type === 'elseif') {
112
+ // Build condition string
113
+ const conditions = block.condition?.map(cond => {
114
+ const varName = `$${cond.flag}`;
115
+ if (cond.operator === 'is_set') {
116
+ return varName;
117
+ }
118
+ else if (cond.operator === 'is_not_set') {
119
+ return `not ${varName}`;
120
+ }
121
+ else if (cond.value !== undefined) {
122
+ const op = cond.operator === 'equals' ? '==' :
123
+ cond.operator === 'not_equals' ? '!=' :
124
+ cond.operator === 'greater_than' ? '>' :
125
+ cond.operator === 'less_than' ? '<' :
126
+ cond.operator === 'greater_equal' ? '>=' :
127
+ cond.operator === 'less_equal' ? '<=' : '==';
128
+ const value = typeof cond.value === 'string' ? `"${cond.value}"` : cond.value;
129
+ return `${varName} ${op} ${value}`;
130
+ }
131
+ return '';
132
+ }).filter(c => c).join(' and ') || '';
133
+ yarn += `<<${block.type} ${conditions}>>\n`;
134
+ }
135
+ else if (block.type === 'else') {
136
+ yarn += `<<else>>\n`;
137
+ }
138
+ // Export block content (remove set commands from content)
139
+ let blockContent = block.content;
140
+ const setCommandsInBlock = blockContent.match(/<<set\s+\$(\w+)\s*([+\-*/=]+)\s*(.+?)>>/g);
141
+ if (setCommandsInBlock) {
142
+ // Remove set commands from content
143
+ setCommandsInBlock.forEach(cmd => {
144
+ blockContent = blockContent.replace(cmd, '').trim();
145
+ });
146
+ }
147
+ if (block.speaker) {
148
+ yarn += `${block.speaker}: ${blockContent.replace(/\n/g, '\n' + block.speaker + ': ')}\n`;
149
+ }
150
+ else {
151
+ yarn += `${blockContent}\n`;
152
+ }
153
+ // Export variable operations from this block
154
+ if (setCommandsInBlock) {
155
+ setCommandsInBlock.forEach(cmd => {
156
+ yarn += `${cmd}\n`;
157
+ });
158
+ }
159
+ // Export block's nextNodeId if present
160
+ if (block.nextNodeId) {
161
+ yarn += `<<jump ${block.nextNodeId}>>\n`;
162
+ }
163
+ });
164
+ yarn += `<<endif>>\n`;
165
+ // Export flags as Yarn variable commands
166
+ const setCommandsInNode = node.content?.match(/<<set\s+\$(\w+)\s*([+\-*/=]+)\s*(.+?)>>/g);
167
+ if (setCommandsInNode) {
168
+ setCommandsInNode.forEach(cmd => {
169
+ yarn += `${cmd}\n`;
170
+ });
171
+ }
172
+ else if (node.setFlags?.length) {
173
+ node.setFlags.forEach(flag => {
174
+ yarn += `<<set $${flag} = true>>\n`;
175
+ });
176
+ }
177
+ // Conditional nodes don't have a main nextNodeId (each block has its own)
178
+ }
179
+ else if (node.type === 'player' && node.choices) {
180
+ node.choices.forEach(choice => {
181
+ // Export conditions as Yarn if statements (wrap the choice)
182
+ if (choice.conditions && choice.conditions.length > 0) {
183
+ // Combine multiple conditions with AND logic
184
+ const conditions = choice.conditions.map(cond => {
185
+ const varName = `$${cond.flag}`;
186
+ if (cond.operator === 'is_set') {
187
+ return varName;
188
+ }
189
+ else if (cond.operator === 'is_not_set') {
190
+ return `not ${varName}`;
191
+ }
192
+ else if (cond.value !== undefined) {
193
+ // Comparison operators
194
+ const op = cond.operator === 'equals' ? '==' :
195
+ cond.operator === 'not_equals' ? '!=' :
196
+ cond.operator === 'greater_than' ? '>' :
197
+ cond.operator === 'less_than' ? '<' :
198
+ cond.operator === 'greater_equal' ? '>=' :
199
+ cond.operator === 'less_equal' ? '<=' : '==';
200
+ const value = typeof cond.value === 'string' ? `"${cond.value}"` : cond.value;
201
+ return `${varName} ${op} ${value}`;
202
+ }
203
+ return '';
204
+ }).filter(c => c).join(' and ');
205
+ if (conditions) {
206
+ yarn += `<<if ${conditions}>>\n`;
207
+ }
208
+ }
209
+ // Remove set commands from choice text before exporting
210
+ let choiceText = choice.text;
211
+ const setCommandsInText = choiceText.match(/<<set\s+\$(\w+)\s*([+\-*/=]+)\s*(.+?)>>/g);
212
+ if (setCommandsInText) {
213
+ // Remove set commands from text
214
+ setCommandsInText.forEach(cmd => {
215
+ choiceText = choiceText.replace(cmd, '').trim();
216
+ });
217
+ }
218
+ yarn += `-> ${choiceText}\n`;
219
+ // Export flags set by this choice
220
+ if (setCommandsInText) {
221
+ // Export the operations found in text
222
+ setCommandsInText.forEach(cmd => {
223
+ yarn += ` ${cmd}\n`;
224
+ });
225
+ }
226
+ else if (choice.setFlags?.length) {
227
+ // Fallback: export as boolean flags
228
+ choice.setFlags.forEach(flag => {
229
+ yarn += ` <<set $${flag} = true>>\n`;
230
+ });
231
+ }
232
+ if (choice.nextNodeId) {
233
+ yarn += ` <<jump ${choice.nextNodeId}>>\n`;
234
+ }
235
+ if (choice.conditions && choice.conditions.length > 0) {
236
+ yarn += `<<endif>>\n`;
237
+ }
238
+ });
239
+ }
240
+ yarn += `===\n\n`;
241
+ });
242
+ return yarn;
243
+ }
244
+ /**
245
+ * Parse Yarn Spinner format to DialogueTree
246
+ */
247
+ export function importFromYarn(yarnContent, title = 'Imported Dialogue') {
248
+ const nodes = {};
249
+ const nodeBlocks = yarnContent.split('===').filter(b => b.trim());
250
+ let y = 50;
251
+ nodeBlocks.forEach((block, idx) => {
252
+ const titleMatch = block.match(/title:\s*(\S+)/);
253
+ if (!titleMatch)
254
+ return;
255
+ const nodeId = titleMatch[1];
256
+ const contentStart = block.indexOf('---');
257
+ if (contentStart === -1)
258
+ return;
259
+ const content = block.slice(contentStart + 3).trim();
260
+ const lines = content.split('\n').filter(l => l.trim());
261
+ const choices = [];
262
+ let dialogueContent = '';
263
+ let speaker = '';
264
+ const setFlags = [];
265
+ let nextNodeId = '';
266
+ const conditionalBlocks = [];
267
+ // Track conditional block state
268
+ let inConditionalBlock = false;
269
+ let currentBlock = null;
270
+ let blockContent = [];
271
+ let blockSpeaker = '';
272
+ // Track conditional choice state (for <<if>> blocks that wrap choices)
273
+ let inConditionalChoice = false;
274
+ let currentChoiceCondition = [];
275
+ const parseCondition = (conditionStr) => {
276
+ // Parse condition string like "$flag", "not $flag", "$flag == 5", etc.
277
+ const conditions = [];
278
+ // Split by 'and' for multiple conditions
279
+ const parts = conditionStr.split(/\s+and\s+/i);
280
+ parts.forEach(part => {
281
+ part = part.trim();
282
+ if (part.startsWith('not ')) {
283
+ const varMatch = part.match(/not\s+\$(\w+)/);
284
+ if (varMatch) {
285
+ conditions.push({ flag: varMatch[1], operator: 'is_not_set' });
286
+ }
287
+ }
288
+ else if (part.includes('==')) {
289
+ const match = part.match(/\$(\w+)\s*==\s*(.+)/);
290
+ if (match) {
291
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
292
+ conditions.push({ flag: match[1], operator: 'equals', value });
293
+ }
294
+ }
295
+ else if (part.includes('!=')) {
296
+ const match = part.match(/\$(\w+)\s*!=\s*(.+)/);
297
+ if (match) {
298
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
299
+ conditions.push({ flag: match[1], operator: 'not_equals', value });
300
+ }
301
+ }
302
+ else if (part.includes('>=')) {
303
+ const match = part.match(/\$(\w+)\s*>=\s*(.+)/);
304
+ if (match) {
305
+ conditions.push({ flag: match[1], operator: 'greater_equal', value: parseFloat(match[2]) });
306
+ }
307
+ }
308
+ else if (part.includes('<=')) {
309
+ const match = part.match(/\$(\w+)\s*<=\s*(.+)/);
310
+ if (match) {
311
+ conditions.push({ flag: match[1], operator: 'less_equal', value: parseFloat(match[2]) });
312
+ }
313
+ }
314
+ else if (part.includes('>')) {
315
+ const match = part.match(/\$(\w+)\s*>\s*(.+)/);
316
+ if (match) {
317
+ conditions.push({ flag: match[1], operator: 'greater_than', value: parseFloat(match[2]) });
318
+ }
319
+ }
320
+ else if (part.includes('<')) {
321
+ const match = part.match(/\$(\w+)\s*<\s*(.+)/);
322
+ if (match) {
323
+ conditions.push({ flag: match[1], operator: 'less_than', value: parseFloat(match[2]) });
324
+ }
325
+ }
326
+ else {
327
+ // Simple flag check
328
+ const varMatch = part.match(/\$(\w+)/);
329
+ if (varMatch) {
330
+ conditions.push({ flag: varMatch[1], operator: 'is_set' });
331
+ }
332
+ }
333
+ });
334
+ return conditions;
335
+ };
336
+ lines.forEach(line => {
337
+ const trimmed = line.trim();
338
+ // Skip empty lines
339
+ if (!trimmed)
340
+ return;
341
+ if (trimmed.startsWith('->')) {
342
+ const choiceText = trimmed.slice(2).trim();
343
+ const choice = {
344
+ id: `c_${Date.now()}_${choices.length}`,
345
+ text: choiceText,
346
+ nextNodeId: ''
347
+ };
348
+ // If we're in a conditional choice context, apply the condition
349
+ if (inConditionalChoice && currentChoiceCondition.length > 0) {
350
+ choice.conditions = currentChoiceCondition;
351
+ // Reset for next choice (if any)
352
+ inConditionalChoice = false;
353
+ currentChoiceCondition = [];
354
+ }
355
+ choices.push(choice);
356
+ }
357
+ else if (trimmed.startsWith('<<jump')) {
358
+ const jumpMatch = trimmed.match(/<<jump\s+(\S+)>>/);
359
+ if (jumpMatch) {
360
+ if (choices.length > 0) {
361
+ choices[choices.length - 1].nextNodeId = jumpMatch[1];
362
+ }
363
+ else {
364
+ nextNodeId = jumpMatch[1];
365
+ }
366
+ }
367
+ }
368
+ else if (trimmed.startsWith('<<set')) {
369
+ // Match: <<set $var = value>>, <<set $var += value>>, etc.
370
+ // Full regex to capture variable name, operator, and value
371
+ const setMatch = trimmed.match(/<<set\s+\$(\w+)\s*([+\-*/=]+)\s*(.+?)>>/);
372
+ if (setMatch) {
373
+ const varName = setMatch[1];
374
+ const operator = setMatch[2].trim();
375
+ const valueStr = setMatch[3].trim();
376
+ // For now, we store the flag name. The operation will be preserved in content
377
+ // when we re-export. In the future, we could extend types to store operations.
378
+ if (choices.length > 0) {
379
+ if (!choices[choices.length - 1].setFlags) {
380
+ choices[choices.length - 1].setFlags = [];
381
+ }
382
+ choices[choices.length - 1].setFlags.push(varName);
383
+ // Store the full command in content for re-export
384
+ if (!choices[choices.length - 1].text.includes('<<set')) {
385
+ choices[choices.length - 1].text += ` ${trimmed}`;
386
+ }
387
+ }
388
+ else {
389
+ setFlags.push(varName);
390
+ // Store the full command in content for re-export
391
+ if (!dialogueContent.includes(trimmed)) {
392
+ dialogueContent += trimmed + '\n';
393
+ }
394
+ }
395
+ }
396
+ }
397
+ else if (trimmed.startsWith('<<if')) {
398
+ // Check if this is a conditional choice (appears before a -> choice)
399
+ // or a conditional block (appears before dialogue content)
400
+ const conditionStr = trimmed.replace(/<<if\s+/, '').replace(/>>/, '').trim();
401
+ const parsedConditions = parseCondition(conditionStr);
402
+ // If we're in a player node context (have choices) or this looks like it's before a choice,
403
+ // treat it as a conditional choice
404
+ if (choices.length > 0 || trimmed.match(/<<if.*>>\s*$/)) {
405
+ inConditionalChoice = true;
406
+ currentChoiceCondition = parsedConditions;
407
+ }
408
+ else {
409
+ // Otherwise, treat it as a conditional block for NPC nodes
410
+ if (inConditionalBlock && currentBlock) {
411
+ // Save previous block
412
+ currentBlock.content = blockContent.join('\n').trim();
413
+ currentBlock.speaker = blockSpeaker || undefined;
414
+ conditionalBlocks.push(currentBlock);
415
+ }
416
+ inConditionalBlock = true;
417
+ currentBlock = {
418
+ id: `block_${Date.now()}_${conditionalBlocks.length}`,
419
+ type: 'if',
420
+ condition: parsedConditions,
421
+ content: '',
422
+ speaker: undefined
423
+ };
424
+ blockContent = [];
425
+ blockSpeaker = '';
426
+ }
427
+ }
428
+ else if (trimmed.startsWith('<<elseif')) {
429
+ // Elseif block
430
+ if (currentBlock) {
431
+ currentBlock.content = blockContent.join('\n').trim();
432
+ currentBlock.speaker = blockSpeaker || undefined;
433
+ conditionalBlocks.push(currentBlock);
434
+ }
435
+ const conditionStr = trimmed.replace(/<<elseif\s+/, '').replace(/>>/, '').trim();
436
+ currentBlock = {
437
+ id: `block_${Date.now()}_${conditionalBlocks.length}`,
438
+ type: 'elseif',
439
+ condition: parseCondition(conditionStr),
440
+ content: '',
441
+ speaker: undefined
442
+ };
443
+ blockContent = [];
444
+ blockSpeaker = '';
445
+ }
446
+ else if (trimmed.startsWith('<<else')) {
447
+ // Else block
448
+ if (currentBlock) {
449
+ currentBlock.content = blockContent.join('\n').trim();
450
+ currentBlock.speaker = blockSpeaker || undefined;
451
+ conditionalBlocks.push(currentBlock);
452
+ }
453
+ currentBlock = {
454
+ id: `block_${Date.now()}_${conditionalBlocks.length}`,
455
+ type: 'else',
456
+ condition: undefined,
457
+ content: '',
458
+ speaker: undefined
459
+ };
460
+ blockContent = [];
461
+ blockSpeaker = '';
462
+ }
463
+ else if (trimmed.startsWith('<<endif')) {
464
+ // End of conditional block or conditional choice
465
+ if (inConditionalChoice) {
466
+ // End of conditional choice - the condition is already applied to the last choice
467
+ inConditionalChoice = false;
468
+ currentChoiceCondition = [];
469
+ }
470
+ else if (currentBlock) {
471
+ // End of conditional block
472
+ currentBlock.content = blockContent.join('\n').trim();
473
+ currentBlock.speaker = blockSpeaker || undefined;
474
+ conditionalBlocks.push(currentBlock);
475
+ inConditionalBlock = false;
476
+ currentBlock = null;
477
+ blockContent = [];
478
+ blockSpeaker = '';
479
+ }
480
+ }
481
+ else if (inConditionalBlock) {
482
+ // Content within conditional block
483
+ if (trimmed.includes(':') && !trimmed.startsWith('<<')) {
484
+ const [spk, ...rest] = trimmed.split(':');
485
+ blockSpeaker = spk.trim();
486
+ blockContent.push(rest.join(':').trim());
487
+ }
488
+ else if (!trimmed.startsWith('<<')) {
489
+ blockContent.push(trimmed);
490
+ }
491
+ }
492
+ else if (trimmed.includes(':') && !trimmed.startsWith('<<')) {
493
+ const [spk, ...rest] = trimmed.split(':');
494
+ speaker = spk.trim();
495
+ dialogueContent += rest.join(':').trim() + '\n';
496
+ }
497
+ else if (!trimmed.startsWith('<<')) {
498
+ dialogueContent += trimmed + '\n';
499
+ }
500
+ });
501
+ nodes[nodeId] = {
502
+ id: nodeId,
503
+ type: choices.length > 0 ? 'player' : 'npc',
504
+ speaker: speaker || undefined,
505
+ content: dialogueContent.trim(),
506
+ choices: choices.length > 0 ? choices : undefined,
507
+ nextNodeId: nextNodeId || undefined,
508
+ setFlags: setFlags.length > 0 ? setFlags : undefined,
509
+ conditionalBlocks: conditionalBlocks.length > 0 ? conditionalBlocks : undefined,
510
+ x: (idx % 3) * 250,
511
+ y: y + Math.floor(idx / 3) * 180
512
+ };
513
+ });
514
+ const startNodeId = Object.keys(nodes)[0] || 'start';
515
+ return {
516
+ id: 'imported',
517
+ title,
518
+ startNodeId,
519
+ nodes
520
+ };
521
+ }