@magicborn/dialogue-forge 0.1.2 → 0.1.4

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.
@@ -36,9 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.PlayView = PlayView;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const flag_manager_1 = require("../lib/flag-manager");
39
- const yarn_runner_1 = require("../lib/yarn-runner");
39
+ const ScenePlayer_1 = require("./ScenePlayer");
40
40
  function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
41
- const [currentNodeId, setCurrentNodeId] = (0, react_1.useState)(startNodeId || dialogue.startNodeId);
42
41
  // Initialize game flags with defaults from schema, then merge with initialFlags
43
42
  const initialGameFlags = (0, react_1.useMemo)(() => {
44
43
  if (flagSchema) {
@@ -47,170 +46,42 @@ function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
47
46
  }
48
47
  return initialFlags || {};
49
48
  }, [flagSchema, initialFlags]);
50
- // Initialize variable manager
51
- const variableManager = (0, react_1.useMemo)(() => {
52
- return new yarn_runner_1.VariableManager(initialGameFlags, new Set());
49
+ // Convert flags to gameState format for ScenePlayer
50
+ const gameState = (0, react_1.useMemo)(() => {
51
+ return { flags: initialGameFlags };
53
52
  }, [initialGameFlags]);
54
- const [gameFlags, setGameFlags] = (0, react_1.useState)(initialGameFlags);
55
- const [history, setHistory] = (0, react_1.useState)([]);
56
- const [isTyping, setIsTyping] = (0, react_1.useState)(false);
53
+ const [currentFlags, setCurrentFlags] = (0, react_1.useState)(initialGameFlags);
57
54
  const [showDebugPanel, setShowDebugPanel] = (0, react_1.useState)(false);
58
- const chatEndRef = (0, react_1.useRef)(null);
59
- // Track which flags were set during this run
60
55
  const [flagsSetDuringRun, setFlagsSetDuringRun] = (0, react_1.useState)(new Set());
61
- // Use ref to track latest gameFlags to avoid stale closures
62
- const gameFlagsRef = (0, react_1.useRef)(gameFlags);
63
- (0, react_1.useEffect)(() => {
64
- gameFlagsRef.current = gameFlags;
65
- }, [gameFlags]);
66
- // Process current node
67
- (0, react_1.useEffect)(() => {
68
- const node = dialogue.nodes[currentNodeId];
69
- if (!node)
70
- return;
71
- // Update variable manager with current game flags (use ref to get latest)
72
- Object.entries(gameFlagsRef.current).forEach(([key, value]) => {
73
- variableManager.set(key, value);
74
- });
75
- // If it's a player node, just ensure typing is false and show choices
76
- if (node.type === 'player') {
77
- setIsTyping(false);
78
- return;
56
+ // Track initial flags to detect changes
57
+ const initialFlagsRef = (0, react_1.useRef)(initialGameFlags);
58
+ const handleComplete = (result) => {
59
+ // Update flags from result
60
+ if (result.updatedFlags) {
61
+ setCurrentFlags(result.updatedFlags);
79
62
  }
80
- // Process the node using the modular runner
81
- setIsTyping(true);
82
- const timer = setTimeout(() => {
83
- // Update flags before processing
84
- if (node.setFlags) {
85
- // Update dialogue flags (temporary)
86
- node.setFlags.forEach(flagId => {
87
- variableManager.addMemoryFlag(flagId);
88
- });
89
- // Update game flags (persistent)
90
- if (flagSchema) {
91
- const gameFlagIds = node.setFlags.filter(flagId => {
92
- const flag = flagSchema.flags.find(f => f.id === flagId);
93
- return flag && flag.type !== 'dialogue';
94
- });
95
- if (gameFlagIds.length > 0) {
96
- const updated = (0, flag_manager_1.mergeFlagUpdates)(gameFlagsRef.current, gameFlagIds, flagSchema);
97
- setGameFlags(updated);
98
- // Update variable manager
99
- gameFlagIds.forEach(flagId => {
100
- const flag = flagSchema.flags.find(f => f.id === flagId);
101
- if (flag) {
102
- variableManager.set(flagId, flag.defaultValue ?? true);
103
- }
104
- });
105
- setFlagsSetDuringRun(prev => {
106
- const next = new Set(prev);
107
- gameFlagIds.forEach(f => next.add(f));
108
- return next;
109
- });
63
+ };
64
+ const handleFlagUpdate = (flags) => {
65
+ setCurrentFlags(flags);
66
+ // Track which flags were set during this run
67
+ if (flagSchema) {
68
+ setFlagsSetDuringRun(prev => {
69
+ const next = new Set(prev);
70
+ Object.keys(flags).forEach(flagId => {
71
+ const initialValue = initialFlagsRef.current[flagId];
72
+ const currentValue = flags[flagId];
73
+ if (initialValue !== currentValue) {
74
+ next.add(flagId);
110
75
  }
111
- }
112
- }
113
- // Process variable operations in content (e.g., <<set $var += 10>>)
114
- (0, yarn_runner_1.processVariableOperationsInContent)(node.content, variableManager);
115
- // Process the node
116
- const result = (0, yarn_runner_1.processNode)(node, variableManager);
117
- // Add to history if there's content
118
- if (result.content) {
119
- setHistory(prev => [...prev, {
120
- nodeId: node.id,
121
- type: 'npc',
122
- speaker: result.speaker,
123
- content: result.content
124
- }]);
125
- }
126
- // Update game flags from variable manager after operations
127
- // Use functional update to avoid dependency issues
128
- setGameFlags(prev => {
129
- const updatedVars = variableManager.getAllVariables();
130
- // Filter out undefined values and only update if there are changes
131
- const definedVars = {};
132
- for (const [key, value] of Object.entries(updatedVars)) {
133
- if (value !== undefined)
134
- definedVars[key] = value;
135
- }
136
- const hasChanges = Object.keys(definedVars).some(key => prev[key] !== definedVars[key]);
137
- return hasChanges ? { ...prev, ...definedVars } : prev;
138
- });
139
- setIsTyping(false);
140
- // Navigate to next node if valid
141
- if (result.nextNodeId && (0, yarn_runner_1.isValidNextNode)(result.nextNodeId, dialogue.nodes)) {
142
- setTimeout(() => setCurrentNodeId(result.nextNodeId), 300);
143
- }
144
- }, 500);
145
- return () => clearTimeout(timer);
146
- }, [currentNodeId, dialogue.startNodeId, flagSchema]); // Removed gameFlags and variableManager from deps
147
- (0, react_1.useEffect)(() => {
148
- chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
149
- }, [history, isTyping]);
150
- const currentNode = dialogue.nodes[currentNodeId];
151
- // Get available choices from processed node result
152
- const processedResult = currentNode ? (0, yarn_runner_1.processNode)(currentNode, variableManager) : null;
153
- const availableChoices = processedResult?.choices || [];
154
- const handleChoice = (choice) => {
155
- setHistory(prev => [...prev, {
156
- nodeId: choice.id,
157
- type: 'player',
158
- content: choice.text
159
- }]);
160
- // Process variable operations in choice text
161
- (0, yarn_runner_1.processVariableOperationsInContent)(choice.text, variableManager);
162
- // Update game flags from variable manager after operations
163
- const updatedVars = variableManager.getAllVariables();
164
- const definedVars = {};
165
- for (const [key, value] of Object.entries(updatedVars)) {
166
- if (value !== undefined)
167
- definedVars[key] = value;
168
- }
169
- setGameFlags(prev => ({ ...prev, ...definedVars }));
170
- if (choice.setFlags) {
171
- // Update dialogue flags (temporary)
172
- choice.setFlags.forEach(flagId => {
173
- variableManager.addMemoryFlag(flagId);
174
- });
175
- // Update game flags (persistent)
176
- if (flagSchema) {
177
- const gameFlagIds = choice.setFlags.filter(flagId => {
178
- const flag = flagSchema.flags.find(f => f.id === flagId);
179
- return flag && flag.type !== 'dialogue';
180
76
  });
181
- if (gameFlagIds.length > 0) {
182
- setGameFlags(prev => (0, flag_manager_1.mergeFlagUpdates)(prev, gameFlagIds, flagSchema));
183
- // Update variable manager
184
- gameFlagIds.forEach(flagId => {
185
- const flag = flagSchema.flags.find(f => f.id === flagId);
186
- if (flag) {
187
- variableManager.set(flagId, flag.defaultValue ?? true);
188
- }
189
- });
190
- setFlagsSetDuringRun(prev => {
191
- const next = new Set(prev);
192
- gameFlagIds.forEach(f => next.add(f));
193
- return next;
194
- });
195
- }
196
- }
197
- }
198
- // Only move to next node if it exists and is valid
199
- if (choice.nextNodeId && (0, yarn_runner_1.isValidNextNode)(choice.nextNodeId, dialogue.nodes)) {
200
- setCurrentNodeId(choice.nextNodeId);
201
- }
202
- else {
203
- // Choice leads nowhere - dialogue complete
204
- setIsTyping(false);
77
+ return next;
78
+ });
205
79
  }
206
80
  };
207
- const handleRestart = () => {
208
- setHistory([]);
209
- variableManager.reset(initialGameFlags, new Set());
210
- setGameFlags(initialGameFlags);
211
- setFlagsSetDuringRun(new Set());
212
- setCurrentNodeId(startNodeId || dialogue.startNodeId);
213
- };
81
+ // Update gameState when flags change (for ScenePlayer)
82
+ const currentGameState = (0, react_1.useMemo)(() => {
83
+ return { flags: currentFlags };
84
+ }, [currentFlags]);
214
85
  // Get all non-dialogue flags from schema
215
86
  const gameFlagsList = (0, react_1.useMemo)(() => {
216
87
  if (!flagSchema)
@@ -250,7 +121,7 @@ function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
250
121
  const flag = flagSchema.flags.find(f => f.id === flagId);
251
122
  if (!flag)
252
123
  return null;
253
- const value = gameFlags[flagId];
124
+ const value = currentFlags[flagId];
254
125
  return (react_1.default.createElement("div", { key: flagId, className: "bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1.5 text-xs" },
255
126
  react_1.default.createElement("div", { className: "flex items-center gap-2" },
256
127
  react_1.default.createElement("span", { className: `px-1.5 py-0.5 rounded text-[10px] border ${flagTypeColors[flag.type]}` }, flag.type),
@@ -265,7 +136,7 @@ function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
265
136
  gameFlagsList.length,
266
137
  ")"),
267
138
  react_1.default.createElement("div", { className: "space-y-1 max-h-96 overflow-y-auto" }, gameFlagsList.map(flag => {
268
- const value = gameFlags[flag.id];
139
+ const value = currentFlags[flag.id];
269
140
  const wasSet = flagsSetDuringRun.has(flag.id);
270
141
  const hasValue = value !== undefined;
271
142
  return (react_1.default.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]'}` },
@@ -280,28 +151,5 @@ function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
280
151
  typeof value === 'number' ? value :
281
152
  `"${value}"`))) : (react_1.default.createElement("div", { className: "mt-1 text-[10px] text-gray-600 italic" }, "Not set"))));
282
153
  })))))),
283
- react_1.default.createElement("div", { className: "flex-1 overflow-y-auto p-4" },
284
- react_1.default.createElement("div", { className: "max-w-2xl mx-auto space-y-4" },
285
- history.map((entry, idx) => (react_1.default.createElement("div", { key: idx, className: `flex ${entry.type === 'player' ? 'justify-end' : 'justify-start'}` },
286
- react_1.default.createElement("div", { className: `max-w-[80%] rounded-2xl px-4 py-3 ${entry.type === 'player'
287
- ? 'bg-[#e94560] text-white rounded-br-md'
288
- : 'bg-[#1a1a2e] text-gray-100 rounded-bl-md'}` },
289
- entry.type === 'npc' && entry.speaker && (react_1.default.createElement("div", { className: "text-xs text-[#e94560] font-medium mb-1" }, entry.speaker)),
290
- react_1.default.createElement("div", { className: "whitespace-pre-wrap" }, entry.content))))),
291
- isTyping && (react_1.default.createElement("div", { className: "flex justify-start" },
292
- react_1.default.createElement("div", { className: "bg-[#1a1a2e] rounded-2xl rounded-bl-md px-4 py-3" },
293
- react_1.default.createElement("div", { className: "flex gap-1" },
294
- react_1.default.createElement("span", { className: "w-2 h-2 bg-[#e94560] rounded-full animate-bounce", style: { animationDelay: '0ms' } }),
295
- react_1.default.createElement("span", { className: "w-2 h-2 bg-[#e94560] rounded-full animate-bounce", style: { animationDelay: '150ms' } }),
296
- react_1.default.createElement("span", { className: "w-2 h-2 bg-[#e94560] rounded-full animate-bounce", style: { animationDelay: '300ms' } }))))),
297
- react_1.default.createElement("div", { ref: chatEndRef }))),
298
- currentNode?.type === 'player' && !isTyping && availableChoices.length > 0 && (react_1.default.createElement("div", { className: "border-t border-[#1a1a2e] bg-[#0d0d14]/80 backdrop-blur-sm p-4" },
299
- react_1.default.createElement("div", { className: "max-w-2xl mx-auto space-y-2" }, availableChoices.map((choice) => (react_1.default.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" },
300
- react_1.default.createElement("span", null, choice.text),
301
- react_1.default.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" },
302
- react_1.default.createElement("polyline", { points: "9 18 15 12 9 6" })))))))),
303
- currentNode?.type === 'npc' && !currentNode.nextNodeId && !isTyping && (react_1.default.createElement("div", { className: "border-t border-[#1a1a2e] bg-[#0d0d14]/80 backdrop-blur-sm p-4" },
304
- react_1.default.createElement("div", { className: "max-w-2xl mx-auto text-center" },
305
- react_1.default.createElement("p", { className: "text-gray-500 mb-3" }, "End of dialogue"),
306
- react_1.default.createElement("button", { onClick: handleRestart, className: "px-4 py-2 bg-[#e94560] hover:bg-[#d63850] text-white rounded-lg transition-colors" }, "Play Again"))))));
154
+ react_1.default.createElement(ScenePlayer_1.ScenePlayer, { dialogue: dialogue, gameState: currentGameState, startNodeId: startNodeId, onComplete: handleComplete, onFlagUpdate: handleFlagUpdate })));
307
155
  }
@@ -100,11 +100,9 @@ function ScenePlayer({ dialogue, gameState, startNodeId, onComplete, onFlagUpdat
100
100
  setIsTyping(false);
101
101
  // Call onNodeExit before moving to next
102
102
  onNodeExit?.(currentNodeId, node);
103
- // Auto-advance if there's a next node
104
- if (node.nextNodeId) {
105
- setTimeout(() => setCurrentNodeId(node.nextNodeId), 300);
106
- }
107
- else {
103
+ // For NPC-only linear stories: don't auto-advance, wait for user input (Enter key or Continue button)
104
+ // Only auto-advance if there's no next node (dialogue complete)
105
+ if (!node.nextNodeId) {
108
106
  // Dialogue complete
109
107
  onDialogueEnd?.();
110
108
  onComplete({
@@ -113,6 +111,7 @@ function ScenePlayer({ dialogue, gameState, startNodeId, onComplete, onFlagUpdat
113
111
  completedNodeIds: Array.from(visitedNodes)
114
112
  });
115
113
  }
114
+ // If there's a nextNodeId, we'll wait for user to press Enter or click Continue
116
115
  }, 500);
117
116
  return () => clearTimeout(timer);
118
117
  }
@@ -131,6 +130,26 @@ function ScenePlayer({ dialogue, gameState, startNodeId, onComplete, onFlagUpdat
131
130
  return cond.operator === 'is_set' ? hasFlag : !hasFlag;
132
131
  });
133
132
  }) || [];
133
+ // Handle Enter key for advancing NPC-only dialogues
134
+ (0, react_1.useEffect)(() => {
135
+ const handleKeyDown = (e) => {
136
+ // Only handle Enter key when:
137
+ // 1. Not typing
138
+ // 2. Current node is NPC
139
+ // 3. There's a next node to advance to
140
+ // 4. Not waiting for player choice
141
+ if (e.key === 'Enter' &&
142
+ !isTyping &&
143
+ currentNode?.type === 'npc' &&
144
+ currentNode.nextNodeId &&
145
+ availableChoices.length === 0) {
146
+ e.preventDefault();
147
+ setCurrentNodeId(currentNode.nextNodeId);
148
+ }
149
+ };
150
+ window.addEventListener('keydown', handleKeyDown);
151
+ return () => window.removeEventListener('keydown', handleKeyDown);
152
+ }, [isTyping, currentNode, availableChoices, setCurrentNodeId]);
134
153
  const handleChoice = (choice) => {
135
154
  const currentNode = dialogue.nodes[currentNodeId];
136
155
  // Call onChoiceSelect hook
@@ -164,6 +183,15 @@ function ScenePlayer({ dialogue, gameState, startNodeId, onComplete, onFlagUpdat
164
183
  });
165
184
  }
166
185
  };
186
+ console.log("isnpc", currentNode?.type === 'npc');
187
+ console.log("isplayer", currentNode?.type === 'player');
188
+ console.log("isTyping", isTyping);
189
+ console.log("availableChoices", availableChoices);
190
+ console.log("visitedNodes", visitedNodes);
191
+ console.log("flags", flags);
192
+ console.log("history", history);
193
+ console.log("currentNodeId", currentNodeId);
194
+ console.log("dialogue", dialogue);
167
195
  return (react_1.default.createElement("div", { className: "flex-1 flex flex-col" },
168
196
  react_1.default.createElement("div", { className: "flex-1 overflow-y-auto p-4" },
169
197
  react_1.default.createElement("div", { className: "max-w-2xl mx-auto space-y-4" },
@@ -192,5 +220,12 @@ function ScenePlayer({ dialogue, gameState, startNodeId, onComplete, onFlagUpdat
192
220
  updatedFlags: flags,
193
221
  dialogueTree: dialogue,
194
222
  completedNodeIds: Array.from(visitedNodes)
195
- }), className: "px-4 py-2 bg-[#e94560] hover:bg-[#d63850] text-white rounded-lg transition-colors" }, "Close"))))));
223
+ }), className: "px-4 py-2 bg-[#e94560] hover:bg-[#d63850] text-white rounded-lg transition-colors" }, "Close")))),
224
+ currentNode?.type === 'npc' && currentNode.nextNodeId && !isTyping && (react_1.default.createElement("div", { className: "border-t border-[#1a1a2e] bg-[#0d0d14]/80 backdrop-blur-sm p-4 sticky bottom-0 z-10" },
225
+ react_1.default.createElement("div", { className: "max-w-2xl mx-auto text-center" },
226
+ react_1.default.createElement("p", { className: "text-xs text-gray-400 mb-3" },
227
+ "Press ",
228
+ react_1.default.createElement("kbd", { className: "px-2 py-1 bg-[#1a1a2e] border border-[#2a2a3e] rounded text-xs" }, "Enter"),
229
+ " to continue"),
230
+ react_1.default.createElement("button", { onClick: () => setCurrentNodeId(currentNode.nextNodeId), className: "px-6 py-3 bg-[#e94560] hover:bg-[#d63850] text-white rounded-lg transition-colors font-medium shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95", autoFocus: true }, "Continue \u2192"))))));
196
231
  }
@@ -6,10 +6,9 @@
6
6
  */
7
7
  import React from 'react';
8
8
  import 'reactflow/dist/style.css';
9
- import { DialogueEditorProps } from '../types';
9
+ import { DialogueEditorProps, ViewMode } from '../types';
10
10
  import { FlagSchema } from '../types/flags';
11
11
  import { Character } from '../types/characters';
12
- type ViewMode = 'graph' | 'yarn' | 'play';
13
12
  export declare function DialogueEditorV2(props: DialogueEditorProps & {
14
13
  flagSchema?: FlagSchema;
15
14
  characters?: Record<string, Character>;
@@ -21,4 +20,3 @@ export declare function DialogueEditorV2(props: DialogueEditorProps & {
21
20
  onOpenFlagManager?: () => void;
22
21
  onOpenGuide?: () => void;
23
22
  }): React.JSX.Element;
24
- export {};
@@ -1,8 +1,7 @@
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';
1
+ import React, { useState, useMemo, useRef } from 'react';
2
+ import { initializeFlags } from '../lib/flag-manager';
3
+ import { ScenePlayer } from './ScenePlayer';
4
4
  export function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
5
- const [currentNodeId, setCurrentNodeId] = useState(startNodeId || dialogue.startNodeId);
6
5
  // Initialize game flags with defaults from schema, then merge with initialFlags
7
6
  const initialGameFlags = useMemo(() => {
8
7
  if (flagSchema) {
@@ -11,170 +10,42 @@ export function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
11
10
  }
12
11
  return initialFlags || {};
13
12
  }, [flagSchema, initialFlags]);
14
- // Initialize variable manager
15
- const variableManager = useMemo(() => {
16
- return new VariableManager(initialGameFlags, new Set());
13
+ // Convert flags to gameState format for ScenePlayer
14
+ const gameState = useMemo(() => {
15
+ return { flags: initialGameFlags };
17
16
  }, [initialGameFlags]);
18
- const [gameFlags, setGameFlags] = useState(initialGameFlags);
19
- const [history, setHistory] = useState([]);
20
- const [isTyping, setIsTyping] = useState(false);
17
+ const [currentFlags, setCurrentFlags] = useState(initialGameFlags);
21
18
  const [showDebugPanel, setShowDebugPanel] = useState(false);
22
- const chatEndRef = useRef(null);
23
- // Track which flags were set during this run
24
19
  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;
20
+ // Track initial flags to detect changes
21
+ const initialFlagsRef = useRef(initialGameFlags);
22
+ const handleComplete = (result) => {
23
+ // Update flags from result
24
+ if (result.updatedFlags) {
25
+ setCurrentFlags(result.updatedFlags);
43
26
  }
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
- });
27
+ };
28
+ const handleFlagUpdate = (flags) => {
29
+ setCurrentFlags(flags);
30
+ // Track which flags were set during this run
31
+ if (flagSchema) {
32
+ setFlagsSetDuringRun(prev => {
33
+ const next = new Set(prev);
34
+ Object.keys(flags).forEach(flagId => {
35
+ const initialValue = initialFlagsRef.current[flagId];
36
+ const currentValue = flags[flagId];
37
+ if (initialValue !== currentValue) {
38
+ next.add(flagId);
74
39
  }
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
40
  });
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);
41
+ return next;
42
+ });
169
43
  }
170
44
  };
171
- const handleRestart = () => {
172
- setHistory([]);
173
- variableManager.reset(initialGameFlags, new Set());
174
- setGameFlags(initialGameFlags);
175
- setFlagsSetDuringRun(new Set());
176
- setCurrentNodeId(startNodeId || dialogue.startNodeId);
177
- };
45
+ // Update gameState when flags change (for ScenePlayer)
46
+ const currentGameState = useMemo(() => {
47
+ return { flags: currentFlags };
48
+ }, [currentFlags]);
178
49
  // Get all non-dialogue flags from schema
179
50
  const gameFlagsList = useMemo(() => {
180
51
  if (!flagSchema)
@@ -214,7 +85,7 @@ export function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
214
85
  const flag = flagSchema.flags.find(f => f.id === flagId);
215
86
  if (!flag)
216
87
  return null;
217
- const value = gameFlags[flagId];
88
+ const value = currentFlags[flagId];
218
89
  return (React.createElement("div", { key: flagId, className: "bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1.5 text-xs" },
219
90
  React.createElement("div", { className: "flex items-center gap-2" },
220
91
  React.createElement("span", { className: `px-1.5 py-0.5 rounded text-[10px] border ${flagTypeColors[flag.type]}` }, flag.type),
@@ -229,7 +100,7 @@ export function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
229
100
  gameFlagsList.length,
230
101
  ")"),
231
102
  React.createElement("div", { className: "space-y-1 max-h-96 overflow-y-auto" }, gameFlagsList.map(flag => {
232
- const value = gameFlags[flag.id];
103
+ const value = currentFlags[flag.id];
233
104
  const wasSet = flagsSetDuringRun.has(flag.id);
234
105
  const hasValue = value !== undefined;
235
106
  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]'}` },
@@ -244,28 +115,5 @@ export function PlayView({ dialogue, startNodeId, flagSchema, initialFlags }) {
244
115
  typeof value === 'number' ? value :
245
116
  `"${value}"`))) : (React.createElement("div", { className: "mt-1 text-[10px] text-gray-600 italic" }, "Not set"))));
246
117
  })))))),
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"))))));
118
+ React.createElement(ScenePlayer, { dialogue: dialogue, gameState: currentGameState, startNodeId: startNodeId, onComplete: handleComplete, onFlagUpdate: handleFlagUpdate })));
271
119
  }