@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,12 @@
1
+ import React from 'react';
2
+ import { DialogueTree } from '../types';
3
+ import { FlagSchema } from '../types/flags';
4
+ import { GameFlagState } from '../types/game-state';
5
+ interface PlayViewProps {
6
+ dialogue: DialogueTree;
7
+ startNodeId?: string;
8
+ flagSchema?: FlagSchema;
9
+ initialFlags?: GameFlagState;
10
+ }
11
+ export declare function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }: PlayViewProps): React.JSX.Element;
12
+ export {};
@@ -0,0 +1,271 @@
1
+ import React, { useState, useEffect, useRef, useMemo } from 'react';
2
+ import { mergeFlagUpdates, initializeFlags } from '../lib/flag-manager';
3
+ import { VariableManager, processNode, isValidNextNode, processVariableOperationsInContent } from '../lib/yarn-runner';
4
+ export function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
5
+ const [currentNodeId, setCurrentNodeId] = useState(startNodeId || dialogue.startNodeId);
6
+ // Initialize game flags with defaults from schema, then merge with initialFlags
7
+ const initialGameFlags = useMemo(() => {
8
+ if (flagSchema) {
9
+ const defaults = initializeFlags(flagSchema);
10
+ return { ...defaults, ...initialFlags };
11
+ }
12
+ return initialFlags || {};
13
+ }, [flagSchema, initialFlags]);
14
+ // Initialize variable manager
15
+ const variableManager = useMemo(() => {
16
+ return new VariableManager(initialGameFlags, new Set());
17
+ }, [initialGameFlags]);
18
+ const [gameFlags, setGameFlags] = useState(initialGameFlags);
19
+ const [history, setHistory] = useState([]);
20
+ const [isTyping, setIsTyping] = useState(false);
21
+ const [showDebugPanel, setShowDebugPanel] = useState(false);
22
+ const chatEndRef = useRef(null);
23
+ // Track which flags were set during this run
24
+ const [flagsSetDuringRun, setFlagsSetDuringRun] = useState(new Set());
25
+ // Use ref to track latest gameFlags to avoid stale closures
26
+ const gameFlagsRef = useRef(gameFlags);
27
+ useEffect(() => {
28
+ gameFlagsRef.current = gameFlags;
29
+ }, [gameFlags]);
30
+ // Process current node
31
+ useEffect(() => {
32
+ const node = dialogue.nodes[currentNodeId];
33
+ if (!node)
34
+ return;
35
+ // Update variable manager with current game flags (use ref to get latest)
36
+ Object.entries(gameFlagsRef.current).forEach(([key, value]) => {
37
+ variableManager.set(key, value);
38
+ });
39
+ // If it's a player node, just ensure typing is false and show choices
40
+ if (node.type === 'player') {
41
+ setIsTyping(false);
42
+ return;
43
+ }
44
+ // Process the node using the modular runner
45
+ setIsTyping(true);
46
+ const timer = setTimeout(() => {
47
+ // Update flags before processing
48
+ if (node.setFlags) {
49
+ // Update dialogue flags (temporary)
50
+ node.setFlags.forEach(flagId => {
51
+ variableManager.addMemoryFlag(flagId);
52
+ });
53
+ // Update game flags (persistent)
54
+ if (flagSchema) {
55
+ const gameFlagIds = node.setFlags.filter(flagId => {
56
+ const flag = flagSchema.flags.find(f => f.id === flagId);
57
+ return flag && flag.type !== 'dialogue';
58
+ });
59
+ if (gameFlagIds.length > 0) {
60
+ const updated = mergeFlagUpdates(gameFlagsRef.current, gameFlagIds, flagSchema);
61
+ setGameFlags(updated);
62
+ // Update variable manager
63
+ gameFlagIds.forEach(flagId => {
64
+ const flag = flagSchema.flags.find(f => f.id === flagId);
65
+ if (flag) {
66
+ variableManager.set(flagId, flag.defaultValue ?? true);
67
+ }
68
+ });
69
+ setFlagsSetDuringRun(prev => {
70
+ const next = new Set(prev);
71
+ gameFlagIds.forEach(f => next.add(f));
72
+ return next;
73
+ });
74
+ }
75
+ }
76
+ }
77
+ // Process variable operations in content (e.g., <<set $var += 10>>)
78
+ processVariableOperationsInContent(node.content, variableManager);
79
+ // Process the node
80
+ const result = processNode(node, variableManager);
81
+ // Add to history if there's content
82
+ if (result.content) {
83
+ setHistory(prev => [...prev, {
84
+ nodeId: node.id,
85
+ type: 'npc',
86
+ speaker: result.speaker,
87
+ content: result.content
88
+ }]);
89
+ }
90
+ // Update game flags from variable manager after operations
91
+ // Use functional update to avoid dependency issues
92
+ setGameFlags(prev => {
93
+ const updatedVars = variableManager.getAllVariables();
94
+ // Filter out undefined values and only update if there are changes
95
+ const definedVars = {};
96
+ for (const [key, value] of Object.entries(updatedVars)) {
97
+ if (value !== undefined)
98
+ definedVars[key] = value;
99
+ }
100
+ const hasChanges = Object.keys(definedVars).some(key => prev[key] !== definedVars[key]);
101
+ return hasChanges ? { ...prev, ...definedVars } : prev;
102
+ });
103
+ setIsTyping(false);
104
+ // Navigate to next node if valid
105
+ if (result.nextNodeId && isValidNextNode(result.nextNodeId, dialogue.nodes)) {
106
+ setTimeout(() => setCurrentNodeId(result.nextNodeId), 300);
107
+ }
108
+ }, 500);
109
+ return () => clearTimeout(timer);
110
+ }, [currentNodeId, dialogue.startNodeId, flagSchema]); // Removed gameFlags and variableManager from deps
111
+ useEffect(() => {
112
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
113
+ }, [history, isTyping]);
114
+ const currentNode = dialogue.nodes[currentNodeId];
115
+ // Get available choices from processed node result
116
+ const processedResult = currentNode ? processNode(currentNode, variableManager) : null;
117
+ const availableChoices = processedResult?.choices || [];
118
+ const handleChoice = (choice) => {
119
+ setHistory(prev => [...prev, {
120
+ nodeId: choice.id,
121
+ type: 'player',
122
+ content: choice.text
123
+ }]);
124
+ // Process variable operations in choice text
125
+ processVariableOperationsInContent(choice.text, variableManager);
126
+ // Update game flags from variable manager after operations
127
+ const updatedVars = variableManager.getAllVariables();
128
+ const definedVars = {};
129
+ for (const [key, value] of Object.entries(updatedVars)) {
130
+ if (value !== undefined)
131
+ definedVars[key] = value;
132
+ }
133
+ setGameFlags(prev => ({ ...prev, ...definedVars }));
134
+ if (choice.setFlags) {
135
+ // Update dialogue flags (temporary)
136
+ choice.setFlags.forEach(flagId => {
137
+ variableManager.addMemoryFlag(flagId);
138
+ });
139
+ // Update game flags (persistent)
140
+ if (flagSchema) {
141
+ const gameFlagIds = choice.setFlags.filter(flagId => {
142
+ const flag = flagSchema.flags.find(f => f.id === flagId);
143
+ return flag && flag.type !== 'dialogue';
144
+ });
145
+ if (gameFlagIds.length > 0) {
146
+ setGameFlags(prev => mergeFlagUpdates(prev, gameFlagIds, flagSchema));
147
+ // Update variable manager
148
+ gameFlagIds.forEach(flagId => {
149
+ const flag = flagSchema.flags.find(f => f.id === flagId);
150
+ if (flag) {
151
+ variableManager.set(flagId, flag.defaultValue ?? true);
152
+ }
153
+ });
154
+ setFlagsSetDuringRun(prev => {
155
+ const next = new Set(prev);
156
+ gameFlagIds.forEach(f => next.add(f));
157
+ return next;
158
+ });
159
+ }
160
+ }
161
+ }
162
+ // Only move to next node if it exists and is valid
163
+ if (choice.nextNodeId && isValidNextNode(choice.nextNodeId, dialogue.nodes)) {
164
+ setCurrentNodeId(choice.nextNodeId);
165
+ }
166
+ else {
167
+ // Choice leads nowhere - dialogue complete
168
+ setIsTyping(false);
169
+ }
170
+ };
171
+ const handleRestart = () => {
172
+ setHistory([]);
173
+ variableManager.reset(initialGameFlags, new Set());
174
+ setGameFlags(initialGameFlags);
175
+ setFlagsSetDuringRun(new Set());
176
+ setCurrentNodeId(startNodeId || dialogue.startNodeId);
177
+ };
178
+ // Get all non-dialogue flags from schema
179
+ const gameFlagsList = useMemo(() => {
180
+ if (!flagSchema)
181
+ return [];
182
+ return flagSchema.flags.filter(f => f.type !== 'dialogue');
183
+ }, [flagSchema]);
184
+ const flagTypeColors = {
185
+ dialogue: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
186
+ quest: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
187
+ achievement: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
188
+ item: 'bg-green-500/20 text-green-400 border-green-500/30',
189
+ stat: 'bg-purple-500/20 text-purple-400 border-purple-500/30',
190
+ title: 'bg-pink-500/20 text-pink-400 border-pink-500/30',
191
+ global: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
192
+ };
193
+ return (React.createElement("main", { className: "flex-1 flex flex-col relative" },
194
+ flagSchema && (React.createElement("button", { onClick: () => setShowDebugPanel(!showDebugPanel), className: "absolute top-4 right-4 z-10 px-3 py-1.5 bg-[#1a1a2e] hover:bg-[#2a2a3e] border border-[#2a2a3e] hover:border-[#e94560] text-gray-400 hover:text-white text-xs rounded-lg transition-colors flex items-center gap-2", title: "Toggle Flag Debug Panel" },
195
+ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
196
+ React.createElement("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
197
+ React.createElement("path", { d: "M9 9h6M9 15h6M9 12h6" })),
198
+ showDebugPanel ? 'Hide' : 'Debug',
199
+ " Flags")),
200
+ showDebugPanel && flagSchema && (React.createElement("div", { className: "absolute top-12 right-4 w-80 bg-[#0d0d14] border border-[#1a1a2e] rounded-lg shadow-xl z-20 max-h-[calc(100vh-8rem)] overflow-hidden flex flex-col" },
201
+ React.createElement("div", { className: "p-3 border-b border-[#1a1a2e] flex items-center justify-between" },
202
+ React.createElement("h3", { className: "text-sm font-semibold text-white" }, "Flag Debug Panel"),
203
+ React.createElement("button", { onClick: () => setShowDebugPanel(false), className: "p-1 text-gray-400 hover:text-white" },
204
+ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
205
+ React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
206
+ React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))),
207
+ React.createElement("div", { className: "flex-1 overflow-y-auto p-3 space-y-4" },
208
+ flagsSetDuringRun.size > 0 && (React.createElement("div", null,
209
+ React.createElement("h4", { className: "text-xs text-gray-500 uppercase mb-2" },
210
+ "Flags Set This Run (",
211
+ flagsSetDuringRun.size,
212
+ ")"),
213
+ React.createElement("div", { className: "space-y-1" }, Array.from(flagsSetDuringRun).map(flagId => {
214
+ const flag = flagSchema.flags.find(f => f.id === flagId);
215
+ if (!flag)
216
+ return null;
217
+ const value = gameFlags[flagId];
218
+ return (React.createElement("div", { key: flagId, className: "bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1.5 text-xs" },
219
+ React.createElement("div", { className: "flex items-center gap-2" },
220
+ React.createElement("span", { className: `px-1.5 py-0.5 rounded text-[10px] border ${flagTypeColors[flag.type]}` }, flag.type),
221
+ React.createElement("span", { className: "font-mono text-white flex-1 truncate" }, flagId),
222
+ value !== undefined && (React.createElement("span", { className: "text-gray-400" },
223
+ "= ",
224
+ typeof value === 'boolean' ? (value ? 'true' : 'false') : String(value))))));
225
+ })))),
226
+ React.createElement("div", null,
227
+ React.createElement("h4", { className: "text-xs text-gray-500 uppercase mb-2" },
228
+ "All Game Flags (",
229
+ gameFlagsList.length,
230
+ ")"),
231
+ React.createElement("div", { className: "space-y-1 max-h-96 overflow-y-auto" }, gameFlagsList.map(flag => {
232
+ const value = gameFlags[flag.id];
233
+ const wasSet = flagsSetDuringRun.has(flag.id);
234
+ const hasValue = value !== undefined;
235
+ return (React.createElement("div", { key: flag.id, className: `bg-[#12121a] border rounded px-2 py-1.5 text-xs transition-colors ${wasSet ? 'border-[#e94560]/50 bg-[#e94560]/5' : 'border-[#2a2a3e]'}` },
236
+ React.createElement("div", { className: "flex items-center gap-2 mb-1" },
237
+ React.createElement("span", { className: `px-1.5 py-0.5 rounded text-[10px] border ${flagTypeColors[flag.type]}` }, flag.type),
238
+ React.createElement("span", { className: "font-mono text-white flex-1 truncate text-[10px]" }, flag.id),
239
+ wasSet && (React.createElement("span", { className: "text-[10px] px-1 py-0.5 bg-[#e94560]/20 text-[#e94560] rounded" }, "NEW"))),
240
+ React.createElement("div", { className: "text-gray-400 text-[10px] truncate" }, flag.name),
241
+ hasValue ? (React.createElement("div", { className: "mt-1 text-[10px] text-gray-300" },
242
+ React.createElement("span", { className: "text-gray-500" }, "Value: "),
243
+ React.createElement("span", { className: "font-mono" }, typeof value === 'boolean' ? (value ? 'true' : 'false') :
244
+ typeof value === 'number' ? value :
245
+ `"${value}"`))) : (React.createElement("div", { className: "mt-1 text-[10px] text-gray-600 italic" }, "Not set"))));
246
+ })))))),
247
+ React.createElement("div", { className: "flex-1 overflow-y-auto p-4" },
248
+ React.createElement("div", { className: "max-w-2xl mx-auto space-y-4" },
249
+ history.map((entry, idx) => (React.createElement("div", { key: idx, className: `flex ${entry.type === 'player' ? 'justify-end' : 'justify-start'}` },
250
+ React.createElement("div", { className: `max-w-[80%] rounded-2xl px-4 py-3 ${entry.type === 'player'
251
+ ? 'bg-[#e94560] text-white rounded-br-md'
252
+ : 'bg-[#1a1a2e] text-gray-100 rounded-bl-md'}` },
253
+ entry.type === 'npc' && entry.speaker && (React.createElement("div", { className: "text-xs text-[#e94560] font-medium mb-1" }, entry.speaker)),
254
+ React.createElement("div", { className: "whitespace-pre-wrap" }, entry.content))))),
255
+ isTyping && (React.createElement("div", { className: "flex justify-start" },
256
+ React.createElement("div", { className: "bg-[#1a1a2e] rounded-2xl rounded-bl-md px-4 py-3" },
257
+ React.createElement("div", { className: "flex gap-1" },
258
+ React.createElement("span", { className: "w-2 h-2 bg-[#e94560] rounded-full animate-bounce", style: { animationDelay: '0ms' } }),
259
+ React.createElement("span", { className: "w-2 h-2 bg-[#e94560] rounded-full animate-bounce", style: { animationDelay: '150ms' } }),
260
+ React.createElement("span", { className: "w-2 h-2 bg-[#e94560] rounded-full animate-bounce", style: { animationDelay: '300ms' } }))))),
261
+ React.createElement("div", { ref: chatEndRef }))),
262
+ currentNode?.type === 'player' && !isTyping && availableChoices.length > 0 && (React.createElement("div", { className: "border-t border-[#1a1a2e] bg-[#0d0d14]/80 backdrop-blur-sm p-4" },
263
+ React.createElement("div", { className: "max-w-2xl mx-auto space-y-2" }, availableChoices.map((choice) => (React.createElement("button", { key: choice.id, onClick: () => handleChoice(choice), className: "w-full text-left px-4 py-3 rounded-lg border border-[#2a2a3e] hover:border-[#e94560] bg-[#12121a] hover:bg-[#1a1a2e] text-gray-200 transition-all group flex items-center justify-between" },
264
+ React.createElement("span", null, choice.text),
265
+ React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600 group-hover:text-[#e94560] transition-colors" },
266
+ React.createElement("polyline", { points: "9 18 15 12 9 6" })))))))),
267
+ currentNode?.type === 'npc' && !currentNode.nextNodeId && !isTyping && (React.createElement("div", { className: "border-t border-[#1a1a2e] bg-[#0d0d14]/80 backdrop-blur-sm p-4" },
268
+ React.createElement("div", { className: "max-w-2xl mx-auto text-center" },
269
+ React.createElement("p", { className: "text-gray-500 mb-3" }, "End of dialogue"),
270
+ React.createElement("button", { onClick: handleRestart, className: "px-4 py-2 bg-[#e94560] hover:bg-[#d63850] text-white rounded-lg transition-colors" }, "Play Again"))))));
271
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { NodeProps } from 'reactflow';
3
+ import { DialogueNode } from '../types';
4
+ import { FlagSchema } from '../types/flags';
5
+ import { LayoutDirection } from '../utils/layout';
6
+ interface PlayerNodeData {
7
+ node: DialogueNode;
8
+ flagSchema?: FlagSchema;
9
+ isDimmed?: boolean;
10
+ isInPath?: boolean;
11
+ layoutDirection?: LayoutDirection;
12
+ isStartNode?: boolean;
13
+ isEndNode?: boolean;
14
+ }
15
+ export declare function PlayerNodeV2({ data, selected }: NodeProps<PlayerNodeData>): React.JSX.Element;
16
+ export {};
@@ -0,0 +1,103 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
3
+ import { GitBranch, Play, Flag } from 'lucide-react';
4
+ // Color scheme for choice edges (same as current implementation)
5
+ const CHOICE_COLORS = ['#e94560', '#8b5cf6', '#06b6d4', '#22c55e', '#f59e0b'];
6
+ export function PlayerNodeV2({ data, selected }) {
7
+ const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
8
+ const choices = node.choices || [];
9
+ const updateNodeInternals = useUpdateNodeInternals();
10
+ const headerRef = useRef(null);
11
+ const choiceRefs = useRef([]);
12
+ const [handlePositions, setHandlePositions] = useState([]);
13
+ // Handle positions based on layout direction
14
+ const isHorizontal = layoutDirection === 'LR';
15
+ const targetPosition = isHorizontal ? Position.Left : Position.Top;
16
+ const sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
17
+ // Calculate handle positions based on actual rendered heights
18
+ useEffect(() => {
19
+ if (headerRef.current && choices.length > 0) {
20
+ const positions = [];
21
+ const headerHeight = headerRef.current.offsetHeight;
22
+ let cumulativeHeight = headerHeight;
23
+ choices.forEach((_choice, idx) => {
24
+ const choiceEl = choiceRefs.current[idx];
25
+ if (choiceEl) {
26
+ const choiceHeight = choiceEl.offsetHeight;
27
+ const handleY = cumulativeHeight + (choiceHeight / 2);
28
+ positions.push(handleY);
29
+ cumulativeHeight += choiceHeight;
30
+ }
31
+ else {
32
+ // Fallback: estimate height
33
+ const estimatedHeight = 32; // py-1.5 (12px) + text (~16px) + flags (~4px) = ~32px
34
+ const handleY = cumulativeHeight + (estimatedHeight / 2);
35
+ positions.push(handleY);
36
+ cumulativeHeight += estimatedHeight;
37
+ }
38
+ });
39
+ setHandlePositions(positions);
40
+ // Update React Flow internals after positions are calculated
41
+ setTimeout(() => {
42
+ updateNodeInternals(node.id);
43
+ }, 0);
44
+ }
45
+ }, [choices, node.id, updateNodeInternals]);
46
+ // Update node internals when choices change
47
+ useEffect(() => {
48
+ updateNodeInternals(node.id);
49
+ }, [choices.length, node.id, updateNodeInternals]);
50
+ // Check if this is an end node (player node with no choices that have nextNodeId)
51
+ const hasNoOutgoingConnections = !choices.some(c => c.nextNodeId);
52
+ // Border color based on state
53
+ const borderClass = selected
54
+ ? 'border-df-player-selected shadow-lg shadow-df-glow'
55
+ : isStartNode
56
+ ? 'border-df-start shadow-md'
57
+ : isEndNode
58
+ ? 'border-df-end shadow-md'
59
+ : 'border-df-player-border';
60
+ return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-player-selected/70' : ''} bg-df-player-bg min-w-[200px] relative`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
61
+ isStartNode && (React.createElement("div", { className: "absolute -top-2 -left-2 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
62
+ React.createElement(Play, { size: 8, fill: "currentColor" }),
63
+ " START")),
64
+ isEndNode && (React.createElement("div", { className: "absolute -top-2 -right-2 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
65
+ React.createElement(Flag, { size: 8 }),
66
+ " END")),
67
+ React.createElement(Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
68
+ React.createElement("div", { ref: headerRef, className: "px-3 py-1.5 border-b border-df-control-border bg-df-player-header flex items-center gap-2 rounded-t-lg" },
69
+ React.createElement(GitBranch, { size: 12, className: "text-df-player-selected" }),
70
+ React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary truncate flex-1" }, node.id),
71
+ React.createElement("span", { className: "text-[10px] text-df-text-tertiary" }, "PLAYER")),
72
+ React.createElement("div", { className: "border-t border-df-control-border" }, choices.map((choice, idx) => {
73
+ // Use calculated position or fallback
74
+ const choiceColor = CHOICE_COLORS[idx % CHOICE_COLORS.length];
75
+ return (React.createElement("div", { key: choice.id, ref: el => {
76
+ choiceRefs.current[idx] = el;
77
+ }, className: "px-3 py-1.5 text-[10px] text-df-text-secondary flex items-center gap-2 border-b border-df-control-border last:border-0 relative" },
78
+ React.createElement("div", { className: "flex-1 min-w-0" },
79
+ React.createElement("span", { className: "truncate block bg-df-base border border-df-control-border rounded px-2 py-1 text-df-text-primary" },
80
+ "\"",
81
+ choice.text || 'Empty choice',
82
+ "\""),
83
+ choice.setFlags && choice.setFlags.length > 0 && (React.createElement("div", { className: "mt-0.5 flex flex-wrap gap-0.5" }, choice.setFlags.map(flagId => {
84
+ const flag = flagSchema?.flags.find(f => f.id === flagId);
85
+ const flagType = flag?.type || 'dialogue';
86
+ const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
87
+ flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
88
+ flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
89
+ flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
90
+ flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
91
+ flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
92
+ flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
93
+ 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
94
+ return (React.createElement("span", { key: flagId, className: `text-[7px] px-0.5 py-0 rounded border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
95
+ })))),
96
+ React.createElement(Handle, { type: "source", position: Position.Right, id: `choice-${idx}`, style: {
97
+ top: `50%`,
98
+ transform: `translateY(-50%)`,
99
+ right: '-6px',
100
+ borderColor: choiceColor,
101
+ }, className: "!bg-df-control-bg !border-2 hover:!border-df-player-selected hover:!bg-df-player-selected/20 !w-3 !h-3 !rounded-full" })));
102
+ }))));
103
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Proof of Concept: React Flow Implementation with Custom Choice Edges
3
+ *
4
+ * This demonstrates how we can use React Flow's custom edges feature
5
+ * to implement our choice-based edge system.
6
+ *
7
+ * Key concepts:
8
+ * 1. Dynamic handles on PlayerNode (one handle per choice)
9
+ * 2. Custom ChoiceEdge component that colors based on choice index
10
+ * 3. Edge data stores choiceIndex and choiceId
11
+ *
12
+ * To use this, install: npm install reactflow
13
+ */
14
+ import React from 'react';
15
+ import { DialogueTree } from '../types';
16
+ /**
17
+ * Custom Edge Component for Player Choice Connections
18
+ *
19
+ * This edge:
20
+ * - Colors based on choice index (from edge data)
21
+ * - Uses bezier path for smooth curves
22
+ * - Matches our current visual style
23
+ */
24
+ /**
25
+ * NPC Node Component
26
+ *
27
+ * Features:
28
+ * - Single output handle at bottom
29
+ * - Speaker + content display
30
+ * - Matches current styling
31
+ */
32
+ /**
33
+ * Player Node Component
34
+ *
35
+ * Features:
36
+ * - Dynamic handles: one per choice (positioned on right side)
37
+ * - Each handle positioned at choice's Y offset
38
+ * - Matches current styling
39
+ */
40
+ /**
41
+ * Convert DialogueTree to React Flow format
42
+ */
43
+ export declare function convertDialogueTreeToReactFlow(dialogue: DialogueTree): {
44
+ nodes: any[];
45
+ edges: any[];
46
+ nodeTypes: {};
47
+ edgeTypes: {};
48
+ };
49
+ /**
50
+ * Main React Flow Component (POC)
51
+ *
52
+ * Usage:
53
+ * ```tsx
54
+ * <ReactFlowProvider>
55
+ * <ReactFlowPOC dialogue={dialogueTree} />
56
+ * </ReactFlowProvider>
57
+ * ```
58
+ */
59
+ export declare function ReactFlowPOC({ dialogue }: {
60
+ dialogue: DialogueTree;
61
+ }): React.JSX.Element;