@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.
- package/demo/app/page.tsx +2 -1
- package/demo/tsconfig.tsbuildinfo +1 -0
- package/dist/components/DialogueEditorV2.d.ts +1 -3
- package/dist/components/PlayView.js +32 -184
- package/dist/components/ScenePlayer.js +41 -6
- package/dist/esm/components/DialogueEditorV2.d.ts +1 -3
- package/dist/esm/components/PlayView.js +34 -186
- package/dist/esm/components/ScenePlayer.js +41 -6
- package/dist/esm/examples/examples-registry.js +8 -0
- package/dist/esm/examples/yarn-examples.js +71 -0
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/examples/examples-registry.js +8 -0
- package/dist/examples/yarn-examples.js +71 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
//
|
|
51
|
-
const
|
|
52
|
-
return
|
|
49
|
+
// Convert flags to gameState format for ScenePlayer
|
|
50
|
+
const gameState = (0, react_1.useMemo)(() => {
|
|
51
|
+
return { flags: initialGameFlags };
|
|
53
52
|
}, [initialGameFlags]);
|
|
54
|
-
const [
|
|
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
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
//
|
|
104
|
-
if (
|
|
105
|
-
|
|
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,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
//
|
|
15
|
-
const
|
|
16
|
-
return
|
|
13
|
+
// Convert flags to gameState format for ScenePlayer
|
|
14
|
+
const gameState = useMemo(() => {
|
|
15
|
+
return { flags: initialGameFlags };
|
|
17
16
|
}, [initialGameFlags]);
|
|
18
|
-
const [
|
|
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
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
}
|