@magicborn/dialogue-forge 0.1.0 → 0.1.1
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 +26 -12
- package/demo/public/logo.svg +14 -1
- package/dist/components/CharacterSelector.d.ts +15 -0
- package/dist/components/CharacterSelector.js +125 -0
- package/dist/components/ConditionalNodeV2.d.ts +2 -0
- package/dist/components/ConditionalNodeV2.js +50 -29
- package/dist/components/DialogueEditorV2.d.ts +2 -0
- package/dist/components/DialogueEditorV2.js +43 -6
- package/dist/components/GuidePanel.js +99 -16
- package/dist/components/NPCNodeV2.d.ts +2 -0
- package/dist/components/NPCNodeV2.js +31 -20
- package/dist/components/NodeEditor.d.ts +3 -1
- package/dist/components/NodeEditor.js +42 -13
- package/dist/components/PlayerNodeV2.d.ts +2 -0
- package/dist/components/PlayerNodeV2.js +58 -39
- package/dist/components/ReactFlowPOC.js +3 -3
- package/dist/esm/components/CharacterSelector.d.ts +15 -0
- package/dist/esm/components/CharacterSelector.js +89 -0
- package/dist/esm/components/ConditionalNodeV2.d.ts +2 -0
- package/dist/esm/components/ConditionalNodeV2.js +51 -30
- package/dist/esm/components/DialogueEditorV2.d.ts +2 -0
- package/dist/esm/components/DialogueEditorV2.js +43 -6
- package/dist/esm/components/GuidePanel.js +99 -16
- package/dist/esm/components/NPCNodeV2.d.ts +2 -0
- package/dist/esm/components/NPCNodeV2.js +32 -21
- package/dist/esm/components/NodeEditor.d.ts +3 -1
- package/dist/esm/components/NodeEditor.js +42 -13
- package/dist/esm/components/PlayerNodeV2.d.ts +2 -0
- package/dist/esm/components/PlayerNodeV2.js +59 -40
- package/dist/esm/components/ReactFlowPOC.js +3 -3
- package/dist/esm/examples/example-characters.d.ts +19 -0
- package/dist/esm/examples/example-characters.js +67 -0
- package/dist/esm/examples/index.d.ts +1 -0
- package/dist/esm/examples/index.js +2 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/types/characters.d.ts +15 -0
- package/dist/esm/types/characters.js +6 -0
- package/dist/esm/types/game-state.d.ts +2 -0
- package/dist/esm/types/index.d.ts +2 -0
- package/dist/examples/example-characters.d.ts +19 -0
- package/dist/examples/example-characters.js +73 -0
- package/dist/examples/index.d.ts +1 -0
- package/dist/examples/index.js +7 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -1
- package/dist/types/characters.d.ts +15 -0
- package/dist/types/characters.js +7 -0
- package/dist/types/game-state.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
|
@@ -399,9 +399,9 @@ const [gameState, setGameState] = useState<GameState>({
|
|
|
399
399
|
}}
|
|
400
400
|
flagSchema={flagSchema}
|
|
401
401
|
// Event hooks
|
|
402
|
-
onNodeAdd={(node) =>
|
|
403
|
-
onNodeDelete={(nodeId) =>
|
|
404
|
-
onConnect={(source, target) =>
|
|
402
|
+
onNodeAdd={(node) => {/* Example: handle node add */}}
|
|
403
|
+
onNodeDelete={(nodeId) => {/* Example: handle node delete */}}
|
|
404
|
+
onConnect={(source, target) => {/* Example: handle connect */}}
|
|
405
405
|
/>`, language: "typescript" }),
|
|
406
406
|
react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "6. Define Game State"),
|
|
407
407
|
react_1.default.createElement("p", { className: "text-gray-400 text-sm mb-2" }, "Game state can be any JSON object. Flags should represent the dialogue-relevant portion of your game state."),
|
|
@@ -465,21 +465,21 @@ const gameState: GameState = {
|
|
|
465
465
|
}}
|
|
466
466
|
// Event hooks
|
|
467
467
|
onNodeEnter={(nodeId, node) => {
|
|
468
|
-
|
|
468
|
+
// Example: handle node enter
|
|
469
469
|
// Trigger animations, sound effects, etc.
|
|
470
470
|
}}
|
|
471
471
|
onNodeExit={(nodeId, node) => {
|
|
472
|
-
|
|
472
|
+
// Example: handle node exit
|
|
473
473
|
}}
|
|
474
474
|
onChoiceSelect={(nodeId, choice) => {
|
|
475
|
-
|
|
475
|
+
// Example: handle choice select
|
|
476
476
|
// Track player decisions
|
|
477
477
|
}}
|
|
478
478
|
onDialogueStart={() => {
|
|
479
|
-
|
|
479
|
+
// Example: handle dialogue start
|
|
480
480
|
}}
|
|
481
481
|
onDialogueEnd={() => {
|
|
482
|
-
|
|
482
|
+
// Example: handle dialogue end
|
|
483
483
|
}}
|
|
484
484
|
/>`, language: "typescript" }),
|
|
485
485
|
react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Complete Example"),
|
|
@@ -538,23 +538,23 @@ const dialogue = importFromYarn(yarnFile, 'Merchant');
|
|
|
538
538
|
flagSchema={flagSchema}
|
|
539
539
|
// Event hooks
|
|
540
540
|
onNodeAdd={(node) => {
|
|
541
|
-
|
|
541
|
+
// Example: handle node add
|
|
542
542
|
// Track node creation
|
|
543
543
|
}}
|
|
544
544
|
onNodeDelete={(nodeId) => {
|
|
545
|
-
|
|
545
|
+
// Example: handle node delete
|
|
546
546
|
}}
|
|
547
547
|
onNodeUpdate={(nodeId, updates) => {
|
|
548
|
-
|
|
548
|
+
// Example: handle node update
|
|
549
549
|
}}
|
|
550
550
|
onConnect={(sourceId, targetId, sourceHandle) => {
|
|
551
|
-
|
|
551
|
+
// Example: handle connect
|
|
552
552
|
}}
|
|
553
553
|
onDisconnect={(edgeId, sourceId, targetId) => {
|
|
554
|
-
|
|
554
|
+
// Example: handle disconnect
|
|
555
555
|
}}
|
|
556
556
|
onNodeSelect={(nodeId) => {
|
|
557
|
-
|
|
557
|
+
// Example: handle node select
|
|
558
558
|
}}
|
|
559
559
|
/>
|
|
560
560
|
|
|
@@ -570,11 +570,11 @@ const dialogue = importFromYarn(yarnFile, 'Merchant');
|
|
|
570
570
|
}));
|
|
571
571
|
}}
|
|
572
572
|
onNodeEnter={(nodeId, node) => {
|
|
573
|
-
|
|
573
|
+
// Example: handle node enter
|
|
574
574
|
// Play animations, sound effects
|
|
575
575
|
}}
|
|
576
576
|
onChoiceSelect={(nodeId, choice) => {
|
|
577
|
-
|
|
577
|
+
// Example: handle choice select
|
|
578
578
|
// Track player decisions
|
|
579
579
|
}}
|
|
580
580
|
/>`, language: "typescript" }),
|
|
@@ -1044,6 +1044,89 @@ await saveFile('dialogue.yarn', newYarn);`))))
|
|
|
1044
1044
|
react_1.default.createElement("strong", { className: "text-white text-xs" }, "Square Selection"),
|
|
1045
1045
|
react_1.default.createElement("p", { className: "text-gray-400 text-xs mt-1" }, "Selection box doesn't always capture all nodes within the selection area (deprioritized)")))))))
|
|
1046
1046
|
},
|
|
1047
|
+
characters: {
|
|
1048
|
+
title: 'Characters',
|
|
1049
|
+
content: (react_1.default.createElement("div", { className: "space-y-4 text-sm" },
|
|
1050
|
+
react_1.default.createElement("p", { className: "text-gray-300" }, "Dialogue Forge supports character assignment for NPC and Player nodes. Characters are defined in your game state and can be selected from a searchable dropdown."),
|
|
1051
|
+
react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Character System"),
|
|
1052
|
+
react_1.default.createElement("p", { className: "text-gray-300 mb-3" }, "Each node (NPC or Player) can be assigned a character from your game state. When a character is assigned:"),
|
|
1053
|
+
react_1.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm ml-2 text-gray-300" },
|
|
1054
|
+
react_1.default.createElement("li", null, "The character's avatar and name are displayed on the node in the graph"),
|
|
1055
|
+
react_1.default.createElement("li", null, "The character name is used as the speaker name"),
|
|
1056
|
+
react_1.default.createElement("li", null, "You can still override with a custom speaker name if needed")),
|
|
1057
|
+
react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Game State Structure"),
|
|
1058
|
+
react_1.default.createElement("p", { className: "text-gray-300 mb-3" },
|
|
1059
|
+
"Characters should be defined in your game state under the ",
|
|
1060
|
+
react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "characters"),
|
|
1061
|
+
" property:"),
|
|
1062
|
+
react_1.default.createElement(CodeBlock_1.CodeBlock, { code: `interface GameState {
|
|
1063
|
+
flags?: FlagState;
|
|
1064
|
+
characters?: {
|
|
1065
|
+
[characterId: string]: Character;
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
interface Character {
|
|
1070
|
+
id: string;
|
|
1071
|
+
name: string;
|
|
1072
|
+
avatar?: string; // URL or emoji (e.g., "👤", "🧙", "/avatars/wizard.png")
|
|
1073
|
+
description?: string;
|
|
1074
|
+
}`, language: "typescript" }),
|
|
1075
|
+
react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Using Characters"),
|
|
1076
|
+
react_1.default.createElement("ol", { className: "list-decimal list-inside space-y-2 text-sm ml-2 text-gray-300" },
|
|
1077
|
+
react_1.default.createElement("li", null,
|
|
1078
|
+
react_1.default.createElement("strong", null, "Define characters"),
|
|
1079
|
+
" in your game state with ",
|
|
1080
|
+
react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "id"),
|
|
1081
|
+
", ",
|
|
1082
|
+
react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "name"),
|
|
1083
|
+
", and optionally ",
|
|
1084
|
+
react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "avatar")),
|
|
1085
|
+
react_1.default.createElement("li", null,
|
|
1086
|
+
react_1.default.createElement("strong", null, "Pass characters"),
|
|
1087
|
+
" to ",
|
|
1088
|
+
react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "DialogueEditorV2"),
|
|
1089
|
+
" via the ",
|
|
1090
|
+
react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "characters"),
|
|
1091
|
+
" prop"),
|
|
1092
|
+
react_1.default.createElement("li", null,
|
|
1093
|
+
react_1.default.createElement("strong", null, "Select a character"),
|
|
1094
|
+
" in the Node Editor using the character dropdown (searchable combobox)"),
|
|
1095
|
+
react_1.default.createElement("li", null,
|
|
1096
|
+
react_1.default.createElement("strong", null, "View on graph"),
|
|
1097
|
+
" - The character's avatar and name appear on the node")),
|
|
1098
|
+
react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Example"),
|
|
1099
|
+
react_1.default.createElement(CodeBlock_1.CodeBlock, { code: `// Game state with characters
|
|
1100
|
+
const gameState = {
|
|
1101
|
+
flags: { reputation: 50 },
|
|
1102
|
+
characters: {
|
|
1103
|
+
stranger: {
|
|
1104
|
+
id: 'stranger',
|
|
1105
|
+
name: 'Mysterious Stranger',
|
|
1106
|
+
avatar: '👤',
|
|
1107
|
+
description: 'A cloaked figure'
|
|
1108
|
+
},
|
|
1109
|
+
player: {
|
|
1110
|
+
id: 'player',
|
|
1111
|
+
name: 'Player',
|
|
1112
|
+
avatar: '🎮',
|
|
1113
|
+
description: 'The player character'
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// Pass to DialogueEditorV2
|
|
1119
|
+
<DialogueEditorV2
|
|
1120
|
+
dialogue={dialogueTree}
|
|
1121
|
+
characters={gameState.characters}
|
|
1122
|
+
flagSchema={flagSchema}
|
|
1123
|
+
onChange={setDialogueTree}
|
|
1124
|
+
/>`, language: "typescript" }),
|
|
1125
|
+
react_1.default.createElement("div", { className: "bg-[#1a2a3e] border-l-4 border-blue-500 p-4 rounded mt-4" },
|
|
1126
|
+
react_1.default.createElement("p", { className: "text-gray-300 text-xs" },
|
|
1127
|
+
react_1.default.createElement("strong", null, "Note:"),
|
|
1128
|
+
" If a character is not assigned, you can still use a custom speaker name. The character system is optional but recommended for consistency across your dialogue system."))))
|
|
1129
|
+
},
|
|
1047
1130
|
theming: {
|
|
1048
1131
|
title: 'Theming',
|
|
1049
1132
|
content: (react_1.default.createElement("div", { className: "space-y-4 text-sm" },
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { NodeProps } from 'reactflow';
|
|
13
13
|
import { DialogueNode } from '../types';
|
|
14
|
+
import { Character } from '../types/characters';
|
|
14
15
|
import { FlagSchema } from '../types/flags';
|
|
15
16
|
import { LayoutDirection } from '../utils/layout';
|
|
16
17
|
interface NPCNodeData {
|
|
17
18
|
node: DialogueNode;
|
|
18
19
|
flagSchema?: FlagSchema;
|
|
20
|
+
characters?: Record<string, Character>;
|
|
19
21
|
isDimmed?: boolean;
|
|
20
22
|
isInPath?: boolean;
|
|
21
23
|
layoutDirection?: LayoutDirection;
|
|
@@ -36,7 +36,11 @@ function getFlagColorClass(type) {
|
|
|
36
36
|
// Component
|
|
37
37
|
// ============================================================================
|
|
38
38
|
function NPCNodeV2({ data, selected }) {
|
|
39
|
-
const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
39
|
+
const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
40
|
+
// Get character if characterId is set
|
|
41
|
+
const character = node.characterId ? characters[node.characterId] : undefined;
|
|
42
|
+
const displayName = character ? character.name : (node.speaker || 'NPC');
|
|
43
|
+
const avatar = character?.avatar || '👤';
|
|
40
44
|
// Handle positions based on layout direction
|
|
41
45
|
const isHorizontal = layoutDirection === 'LR';
|
|
42
46
|
const targetPosition = isHorizontal ? reactflow_1.Position.Left : reactflow_1.Position.Top;
|
|
@@ -59,28 +63,35 @@ function NPCNodeV2({ data, selected }) {
|
|
|
59
63
|
const contentPreview = node.content.length > 60
|
|
60
64
|
? node.content.slice(0, 60) + '...'
|
|
61
65
|
: node.content;
|
|
62
|
-
return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-node-selected/70' : ''} bg-df-npc-bg min-w-[
|
|
63
|
-
isStartNode && (react_1.default.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" },
|
|
64
|
-
react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
|
|
65
|
-
" START")),
|
|
66
|
-
isEndNode && (react_1.default.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" },
|
|
67
|
-
react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
|
|
68
|
-
" END")),
|
|
66
|
+
return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-node-selected/70' : ''} bg-df-npc-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
|
|
69
67
|
react_1.default.createElement(reactflow_1.Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
|
|
70
|
-
react_1.default.createElement("div", { className:
|
|
71
|
-
react_1.default.createElement(
|
|
72
|
-
react_1.default.createElement("
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
react_1.default.createElement("div", { className: `${headerBgClass} border-b-2 border-df-npc-border px-3 py-2.5 flex items-center gap-3 relative` },
|
|
69
|
+
react_1.default.createElement("div", { className: "w-14 h-14 rounded-full bg-df-npc-bg border-[3px] border-df-npc-border flex items-center justify-center text-3xl shadow-lg flex-shrink-0" }, avatar),
|
|
70
|
+
react_1.default.createElement("div", { className: "flex-1 min-w-0" },
|
|
71
|
+
react_1.default.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
|
|
72
|
+
react_1.default.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
|
|
73
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-base/50 border border-df-control-border", title: `Node ID: ${node.id}` },
|
|
74
|
+
react_1.default.createElement(lucide_react_1.Hash, { size: 12, className: "text-df-text-secondary" }),
|
|
75
|
+
react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
|
|
76
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-npc-selected/20 border border-df-npc-selected/50", title: "NPC Node" },
|
|
77
|
+
react_1.default.createElement(lucide_react_1.MessageSquare, { size: 14, className: "text-df-npc-selected" }),
|
|
78
|
+
react_1.default.createElement("span", { className: "text-[10px] font-semibold text-df-npc-selected" }, "NPC"))),
|
|
79
|
+
isStartNode && (react_1.default.createElement("div", { className: "absolute top-1 right-1 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
|
|
80
|
+
react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
|
|
81
|
+
" START")),
|
|
82
|
+
isEndNode && (react_1.default.createElement("div", { className: "absolute top-1 right-1 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
|
|
83
|
+
react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
|
|
84
|
+
" END"))),
|
|
85
|
+
react_1.default.createElement("div", { className: "px-4 py-3" },
|
|
86
|
+
react_1.default.createElement("div", { className: "bg-df-elevated border border-df-control-border rounded-lg px-4 py-3 mb-2" },
|
|
87
|
+
react_1.default.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
|
|
88
|
+
"\"",
|
|
89
|
+
contentPreview,
|
|
90
|
+
"\"")),
|
|
91
|
+
node.setFlags && node.setFlags.length > 0 && (react_1.default.createElement("div", { className: "flex flex-wrap gap-1" }, node.setFlags.map((flagId) => {
|
|
81
92
|
const flag = flagSchema?.flags.find((f) => f.id === flagId);
|
|
82
93
|
const flagType = flag?.type || 'dialogue';
|
|
83
|
-
return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded border ${getFlagColorClass(flagType)}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
|
|
94
|
+
return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1.5 py-0.5 rounded-full border ${getFlagColorClass(flagType)}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
|
|
84
95
|
})))),
|
|
85
96
|
react_1.default.createElement(reactflow_1.Handle, { type: "source", position: sourcePosition, id: "next", className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full hover:!border-df-npc-selected hover:!bg-df-npc-selected/20" })));
|
|
86
97
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { DialogueNode, DialogueTree, Choice } from '../types';
|
|
3
3
|
import { FlagSchema } from '../types/flags';
|
|
4
|
+
import { Character } from '../types/characters';
|
|
4
5
|
interface NodeEditorProps {
|
|
5
6
|
node: DialogueNode;
|
|
6
7
|
dialogue: DialogueTree;
|
|
@@ -13,6 +14,7 @@ interface NodeEditorProps {
|
|
|
13
14
|
onPlayFromHere?: (nodeId: string) => void;
|
|
14
15
|
onFocusNode?: (nodeId: string) => void;
|
|
15
16
|
flagSchema?: FlagSchema;
|
|
17
|
+
characters?: Record<string, Character>;
|
|
16
18
|
}
|
|
17
|
-
export declare function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }: NodeEditorProps): React.JSX.Element;
|
|
19
|
+
export declare function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema, characters, }: NodeEditorProps): React.JSX.Element;
|
|
18
20
|
export {};
|
|
@@ -36,12 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.NodeEditor = NodeEditor;
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const FlagSelector_1 = require("./FlagSelector");
|
|
39
|
+
const CharacterSelector_1 = require("./CharacterSelector");
|
|
39
40
|
const constants_1 = require("../types/constants");
|
|
40
41
|
const lucide_react_1 = require("lucide-react");
|
|
41
42
|
const reactflow_converter_1 = require("../utils/reactflow-converter");
|
|
42
43
|
const EdgeIcon_1 = require("./EdgeIcon");
|
|
43
44
|
const ConditionAutocomplete_1 = require("./ConditionAutocomplete");
|
|
44
|
-
function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }) {
|
|
45
|
+
function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema, characters = {}, }) {
|
|
45
46
|
// Local state for condition input values (keyed by block id for conditional blocks, choice id for choices)
|
|
46
47
|
const [conditionInputs, setConditionInputs] = (0, react_1.useState)({});
|
|
47
48
|
const [debouncedConditionInputs, setDebouncedConditionInputs] = (0, react_1.useState)({});
|
|
@@ -347,11 +348,21 @@ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateC
|
|
|
347
348
|
react_1.default.createElement("input", { value: node.id, disabled: true, className: "w-full bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-xs text-gray-500 font-mono" })),
|
|
348
349
|
node.type === 'npc' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
349
350
|
react_1.default.createElement("div", null,
|
|
350
|
-
react_1.default.createElement("label", { className: "text-[10px] text-
|
|
351
|
+
react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
|
|
352
|
+
react_1.default.createElement(CharacterSelector_1.CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
|
|
353
|
+
const character = characterId ? characters[characterId] : undefined;
|
|
354
|
+
onUpdate({
|
|
355
|
+
characterId,
|
|
356
|
+
speaker: character ? character.name : node.speaker, // Keep speaker as fallback
|
|
357
|
+
});
|
|
358
|
+
}, placeholder: "Select character...", className: "mb-2" }),
|
|
359
|
+
react_1.default.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
|
|
360
|
+
react_1.default.createElement("div", null,
|
|
361
|
+
react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
|
|
351
362
|
react_1.default.createElement("div", { className: "flex items-center gap-2" },
|
|
352
|
-
react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-
|
|
353
|
-
react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-
|
|
354
|
-
react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none", placeholder: "
|
|
363
|
+
react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-df-control-bg border border-df-control-border flex items-center justify-center flex-shrink-0" },
|
|
364
|
+
react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-df-text-secondary" })),
|
|
365
|
+
react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none", placeholder: "Custom speaker name (optional)" }))),
|
|
355
366
|
react_1.default.createElement("div", null,
|
|
356
367
|
react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Content"),
|
|
357
368
|
react_1.default.createElement("textarea", { value: node.content, onChange: (e) => onUpdate({ content: e.target.value }), className: "w-full bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none min-h-[100px] resize-y", placeholder: "What the character says..." })),
|
|
@@ -411,14 +422,22 @@ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateC
|
|
|
411
422
|
return (react_1.default.createElement("div", { key: block.id, className: `rounded p-2 space-y-2 ${styles.bg} ${styles.border} border-2` },
|
|
412
423
|
react_1.default.createElement("div", { className: "flex items-center gap-2" },
|
|
413
424
|
react_1.default.createElement("span", { className: `text-[9px] px-1.5 py-0.5 rounded ${styles.tagBg} ${styles.tagText} font-semibold` }, block.type === 'if' ? 'IF' : block.type === 'elseif' ? 'ELSE IF' : 'ELSE'),
|
|
414
|
-
react_1.default.createElement("div", { className: "flex items-center gap-
|
|
415
|
-
react_1.default.createElement(
|
|
416
|
-
|
|
425
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1.5 flex-1" },
|
|
426
|
+
react_1.default.createElement(CharacterSelector_1.CharacterSelector, { characters: characters, selectedCharacterId: block.characterId, onSelect: (characterId) => {
|
|
427
|
+
const newBlocks = [...node.conditionalBlocks];
|
|
428
|
+
const character = characterId ? characters[characterId] : undefined;
|
|
429
|
+
newBlocks[idx] = {
|
|
430
|
+
...block,
|
|
431
|
+
characterId,
|
|
432
|
+
speaker: character ? character.name : block.speaker, // Keep speaker as fallback
|
|
433
|
+
};
|
|
434
|
+
onUpdate({ conditionalBlocks: newBlocks });
|
|
435
|
+
}, placeholder: "Speaker...", compact: true, className: "flex-1" }),
|
|
417
436
|
react_1.default.createElement("input", { type: "text", value: block.speaker || '', onChange: (e) => {
|
|
418
437
|
const newBlocks = [...node.conditionalBlocks];
|
|
419
438
|
newBlocks[idx] = { ...block, speaker: e.target.value || undefined };
|
|
420
439
|
onUpdate({ conditionalBlocks: newBlocks });
|
|
421
|
-
}, className: `flex-1 bg-
|
|
440
|
+
}, className: `flex-1 bg-df-elevated border border-df-control-border rounded px-1.5 py-0.5 text-[10px] text-df-text-primary focus:border-df-conditional-selected outline-none`, placeholder: "Custom name" }))),
|
|
422
441
|
block.type !== 'else' && (() => {
|
|
423
442
|
const parseCondition = (conditionStr) => {
|
|
424
443
|
const conditions = [];
|
|
@@ -726,11 +745,21 @@ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateC
|
|
|
726
745
|
}, className: "text-xs px-2 py-1 bg-[#12121a] border border-[#2a2a3e] rounded text-gray-400 hover:text-gray-200" }, "+ Add Else"))))) : (react_1.default.createElement("div", { className: "text-xs text-gray-500 p-4 text-center border border-[#2a2a3e] rounded" }, "No conditional blocks. Add an \"If\" block to start."))))),
|
|
727
746
|
node.type === 'player' && (react_1.default.createElement("div", null,
|
|
728
747
|
react_1.default.createElement("div", null,
|
|
729
|
-
react_1.default.createElement("label", { className: "text-[10px] text-
|
|
748
|
+
react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
|
|
749
|
+
react_1.default.createElement(CharacterSelector_1.CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
|
|
750
|
+
const character = characterId ? characters[characterId] : undefined;
|
|
751
|
+
onUpdate({
|
|
752
|
+
characterId,
|
|
753
|
+
speaker: character ? character.name : node.speaker, // Keep speaker as fallback
|
|
754
|
+
});
|
|
755
|
+
}, placeholder: "Select character...", className: "mb-2" }),
|
|
756
|
+
react_1.default.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
|
|
757
|
+
react_1.default.createElement("div", null,
|
|
758
|
+
react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
|
|
730
759
|
react_1.default.createElement("div", { className: "flex items-center gap-2" },
|
|
731
|
-
react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-
|
|
732
|
-
react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-
|
|
733
|
-
react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-
|
|
760
|
+
react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-df-control-bg border border-df-control-border flex items-center justify-center flex-shrink-0" },
|
|
761
|
+
react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-df-text-secondary" })),
|
|
762
|
+
react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-player-selected outline-none", placeholder: "Custom speaker name (optional)" }))),
|
|
734
763
|
react_1.default.createElement("div", { className: "flex items-center justify-between mb-2 mt-4" },
|
|
735
764
|
react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Choices"),
|
|
736
765
|
react_1.default.createElement("button", { onClick: onAddChoice, className: "text-[10px] text-[#e94560] hover:text-[#ff6b6b]" }, "+ Add")),
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { NodeProps } from 'reactflow';
|
|
3
3
|
import { DialogueNode } from '../types';
|
|
4
|
+
import { Character } from '../types/characters';
|
|
4
5
|
import { FlagSchema } from '../types/flags';
|
|
5
6
|
import { LayoutDirection } from '../utils/layout';
|
|
6
7
|
interface PlayerNodeData {
|
|
7
8
|
node: DialogueNode;
|
|
8
9
|
flagSchema?: FlagSchema;
|
|
10
|
+
characters?: Record<string, Character>;
|
|
9
11
|
isDimmed?: boolean;
|
|
10
12
|
isInPath?: boolean;
|
|
11
13
|
layoutDirection?: LayoutDirection;
|
|
@@ -40,8 +40,12 @@ const lucide_react_1 = require("lucide-react");
|
|
|
40
40
|
// Color scheme for choice edges (same as current implementation)
|
|
41
41
|
const CHOICE_COLORS = ['#e94560', '#8b5cf6', '#06b6d4', '#22c55e', '#f59e0b'];
|
|
42
42
|
function PlayerNodeV2({ data, selected }) {
|
|
43
|
-
const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
43
|
+
const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
44
44
|
const choices = node.choices || [];
|
|
45
|
+
// Get character if characterId is set
|
|
46
|
+
const character = node.characterId ? characters[node.characterId] : undefined;
|
|
47
|
+
const displayName = character ? character.name : (node.speaker || 'Player');
|
|
48
|
+
const avatar = character?.avatar || '🎮';
|
|
45
49
|
const updateNodeInternals = (0, reactflow_1.useUpdateNodeInternals)();
|
|
46
50
|
const headerRef = (0, react_1.useRef)(null);
|
|
47
51
|
const choiceRefs = (0, react_1.useRef)([]);
|
|
@@ -93,47 +97,62 @@ function PlayerNodeV2({ data, selected }) {
|
|
|
93
97
|
: isEndNode
|
|
94
98
|
? 'border-df-end shadow-md'
|
|
95
99
|
: 'border-df-player-border';
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// Header background for player nodes
|
|
101
|
+
const headerBgClass = isStartNode
|
|
102
|
+
? 'bg-df-start-bg'
|
|
103
|
+
: isEndNode
|
|
104
|
+
? 'bg-df-end-bg'
|
|
105
|
+
: 'bg-df-player-header';
|
|
106
|
+
return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-player-selected/70' : ''} bg-df-player-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
|
|
103
107
|
react_1.default.createElement(reactflow_1.Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
|
|
104
|
-
react_1.default.createElement("div", { ref: headerRef, className:
|
|
105
|
-
react_1.default.createElement(
|
|
106
|
-
react_1.default.createElement("
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
react_1.default.createElement("div", { ref: headerRef, className: `${headerBgClass} border-b-2 border-df-player-border px-3 py-2.5 flex items-center gap-3 relative` },
|
|
109
|
+
react_1.default.createElement("div", { className: "w-14 h-14 rounded-full bg-df-player-bg border-[3px] border-df-player-border flex items-center justify-center text-3xl shadow-lg flex-shrink-0" }, avatar),
|
|
110
|
+
react_1.default.createElement("div", { className: "flex-1 min-w-0" },
|
|
111
|
+
react_1.default.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
|
|
112
|
+
react_1.default.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
|
|
113
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-base/50 border border-df-control-border", title: `Node ID: ${node.id}` },
|
|
114
|
+
react_1.default.createElement(lucide_react_1.Hash, { size: 12, className: "text-df-text-secondary" }),
|
|
115
|
+
react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
|
|
116
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-player-selected/20 border border-df-player-selected/50", title: "Player Node" },
|
|
117
|
+
react_1.default.createElement(lucide_react_1.GitBranch, { size: 14, className: "text-df-player-selected" }),
|
|
118
|
+
react_1.default.createElement("span", { className: "text-[10px] font-semibold text-df-player-selected" }, "PLAYER"))),
|
|
119
|
+
isStartNode && (react_1.default.createElement("div", { className: "absolute top-1 right-1 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
|
|
120
|
+
react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
|
|
121
|
+
" START")),
|
|
122
|
+
isEndNode && (react_1.default.createElement("div", { className: "absolute top-1 right-1 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
|
|
123
|
+
react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
|
|
124
|
+
" END"))),
|
|
125
|
+
react_1.default.createElement("div", { className: "px-4 py-3 space-y-2" }, choices.map((choice, idx) => {
|
|
109
126
|
// Use calculated position or fallback
|
|
110
127
|
const choiceColor = CHOICE_COLORS[idx % CHOICE_COLORS.length];
|
|
111
128
|
return (react_1.default.createElement("div", { key: choice.id, ref: el => {
|
|
112
129
|
choiceRefs.current[idx] = el;
|
|
113
|
-
}, className: "
|
|
114
|
-
react_1.default.createElement("div", { className: "flex-
|
|
115
|
-
react_1.default.createElement("
|
|
116
|
-
"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
flagType === '
|
|
124
|
-
flagType === '
|
|
125
|
-
flagType === '
|
|
126
|
-
flagType === '
|
|
127
|
-
flagType === '
|
|
128
|
-
flagType === '
|
|
129
|
-
'bg-df-flag-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
}, className: "relative group" },
|
|
131
|
+
react_1.default.createElement("div", { className: "bg-df-elevated border border-df-control-border rounded-lg px-3 py-2 flex items-start gap-2 hover:border-df-player-selected/50 transition-colors" },
|
|
132
|
+
react_1.default.createElement("div", { className: "flex-1 min-w-0" },
|
|
133
|
+
react_1.default.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
|
|
134
|
+
"\"",
|
|
135
|
+
choice.text || 'Empty choice',
|
|
136
|
+
"\""),
|
|
137
|
+
choice.setFlags && choice.setFlags.length > 0 && (react_1.default.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1" }, choice.setFlags.map(flagId => {
|
|
138
|
+
const flag = flagSchema?.flags.find(f => f.id === flagId);
|
|
139
|
+
const flagType = flag?.type || 'dialogue';
|
|
140
|
+
const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
|
|
141
|
+
flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
|
|
142
|
+
flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
|
|
143
|
+
flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
|
|
144
|
+
flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
|
|
145
|
+
flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
|
|
146
|
+
flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
|
|
147
|
+
'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
|
|
148
|
+
return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded-full border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
|
|
149
|
+
})))),
|
|
150
|
+
react_1.default.createElement(reactflow_1.Handle, { type: "source", position: reactflow_1.Position.Right, id: `choice-${idx}`, style: {
|
|
151
|
+
top: `50%`,
|
|
152
|
+
transform: `translateY(-50%)`,
|
|
153
|
+
right: '-6px',
|
|
154
|
+
borderColor: choiceColor,
|
|
155
|
+
}, className: "!bg-df-control-bg !border-2 hover:!border-df-player-selected hover:!bg-df-player-selected/20 !w-3 !h-3 !rounded-full" }))));
|
|
156
|
+
})),
|
|
157
|
+
react_1.default.createElement(reactflow_1.Handle, { type: "source", position: sourcePosition, id: "next", className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full hover:!border-df-player-selected hover:!bg-df-player-selected/20" })));
|
|
139
158
|
}
|
|
@@ -271,17 +271,17 @@ function ReactFlowPOC({ dialogue }) {
|
|
|
271
271
|
const onNodesChange = (0, react_1.useCallback)((changes) => {
|
|
272
272
|
// Update dialogue tree positions
|
|
273
273
|
// This would sync back to our DialogueTree structure
|
|
274
|
-
|
|
274
|
+
// Handle node changes
|
|
275
275
|
}, []);
|
|
276
276
|
// Handle edge connections
|
|
277
277
|
const onConnect = (0, react_1.useCallback)((connection) => {
|
|
278
278
|
// Handle new edge connections
|
|
279
279
|
// This would update our DialogueTree structure
|
|
280
|
-
|
|
280
|
+
// Handle connection
|
|
281
281
|
}, []);
|
|
282
282
|
// Handle edge changes (delete, etc.)
|
|
283
283
|
const onEdgesChange = (0, react_1.useCallback)((changes) => {
|
|
284
|
-
|
|
284
|
+
// Handle edge changes
|
|
285
285
|
}, []);
|
|
286
286
|
return (react_1.default.createElement("div", { className: "w-full h-full" },
|
|
287
287
|
react_1.default.createElement("div", { className: "p-8 text-gray-400" },
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CharacterSelector - Combobox with search for selecting characters
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Character } from '../types/characters';
|
|
6
|
+
interface CharacterSelectorProps {
|
|
7
|
+
characters?: Record<string, Character>;
|
|
8
|
+
selectedCharacterId?: string;
|
|
9
|
+
onSelect: (characterId: string | undefined) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function CharacterSelector({ characters, selectedCharacterId, onSelect, placeholder, className, compact, }: CharacterSelectorProps): React.JSX.Element;
|
|
15
|
+
export {};
|