@magicborn/dialogue-forge 0.1.0 → 0.1.2
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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { Handle, Position } from 'reactflow';
|
|
13
|
-
import { MessageSquare, Play, Flag } from 'lucide-react';
|
|
13
|
+
import { MessageSquare, Play, Flag, Hash } from 'lucide-react';
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// Styles
|
|
16
16
|
// ============================================================================
|
|
@@ -30,7 +30,11 @@ function getFlagColorClass(type) {
|
|
|
30
30
|
// Component
|
|
31
31
|
// ============================================================================
|
|
32
32
|
export function NPCNodeV2({ data, selected }) {
|
|
33
|
-
const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
33
|
+
const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
34
|
+
// Get character if characterId is set
|
|
35
|
+
const character = node.characterId ? characters[node.characterId] : undefined;
|
|
36
|
+
const displayName = character ? character.name : (node.speaker || 'NPC');
|
|
37
|
+
const avatar = character?.avatar || '👤';
|
|
34
38
|
// Handle positions based on layout direction
|
|
35
39
|
const isHorizontal = layoutDirection === 'LR';
|
|
36
40
|
const targetPosition = isHorizontal ? Position.Left : Position.Top;
|
|
@@ -53,28 +57,35 @@ export function NPCNodeV2({ data, selected }) {
|
|
|
53
57
|
const contentPreview = node.content.length > 60
|
|
54
58
|
? node.content.slice(0, 60) + '...'
|
|
55
59
|
: node.content;
|
|
56
|
-
return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-node-selected/70' : ''} bg-df-npc-bg min-w-[
|
|
57
|
-
isStartNode && (React.createElement("div", { className: "absolute -top-2 -left-2 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
|
|
58
|
-
React.createElement(Play, { size: 8, fill: "currentColor" }),
|
|
59
|
-
" START")),
|
|
60
|
-
isEndNode && (React.createElement("div", { className: "absolute -top-2 -right-2 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
|
|
61
|
-
React.createElement(Flag, { size: 8 }),
|
|
62
|
-
" END")),
|
|
60
|
+
return (React.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 },
|
|
63
61
|
React.createElement(Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
|
|
64
|
-
React.createElement("div", { className:
|
|
65
|
-
React.createElement(
|
|
66
|
-
React.createElement("
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
React.createElement("div", { className: `${headerBgClass} border-b-2 border-df-npc-border px-3 py-2.5 flex items-center gap-3 relative` },
|
|
63
|
+
React.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),
|
|
64
|
+
React.createElement("div", { className: "flex-1 min-w-0" },
|
|
65
|
+
React.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
|
|
66
|
+
React.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
|
|
67
|
+
React.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}` },
|
|
68
|
+
React.createElement(Hash, { size: 12, className: "text-df-text-secondary" }),
|
|
69
|
+
React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
|
|
70
|
+
React.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" },
|
|
71
|
+
React.createElement(MessageSquare, { size: 14, className: "text-df-npc-selected" }),
|
|
72
|
+
React.createElement("span", { className: "text-[10px] font-semibold text-df-npc-selected" }, "NPC"))),
|
|
73
|
+
isStartNode && (React.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" },
|
|
74
|
+
React.createElement(Play, { size: 8, fill: "currentColor" }),
|
|
75
|
+
" START")),
|
|
76
|
+
isEndNode && (React.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" },
|
|
77
|
+
React.createElement(Flag, { size: 8 }),
|
|
78
|
+
" END"))),
|
|
79
|
+
React.createElement("div", { className: "px-4 py-3" },
|
|
80
|
+
React.createElement("div", { className: "bg-df-elevated border border-df-control-border rounded-lg px-4 py-3 mb-2" },
|
|
81
|
+
React.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
|
|
82
|
+
"\"",
|
|
83
|
+
contentPreview,
|
|
84
|
+
"\"")),
|
|
85
|
+
node.setFlags && node.setFlags.length > 0 && (React.createElement("div", { className: "flex flex-wrap gap-1" }, node.setFlags.map((flagId) => {
|
|
75
86
|
const flag = flagSchema?.flags.find((f) => f.id === flagId);
|
|
76
87
|
const flagType = flag?.type || 'dialogue';
|
|
77
|
-
return (React.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]));
|
|
88
|
+
return (React.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]));
|
|
78
89
|
})))),
|
|
79
90
|
React.createElement(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" })));
|
|
80
91
|
}
|
|
@@ -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 {};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import { FlagSelector } from './FlagSelector';
|
|
3
|
+
import { CharacterSelector } from './CharacterSelector';
|
|
3
4
|
import { CONDITION_OPERATOR } from '../types/constants';
|
|
4
5
|
import { AlertCircle, CheckCircle, Info, GitBranch, X, User, Maximize2 } from 'lucide-react';
|
|
5
6
|
import { CHOICE_COLORS } from '../utils/reactflow-converter';
|
|
6
7
|
import { EdgeIcon } from './EdgeIcon';
|
|
7
8
|
import { ConditionAutocomplete } from './ConditionAutocomplete';
|
|
8
|
-
export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }) {
|
|
9
|
+
export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema, characters = {}, }) {
|
|
9
10
|
// Local state for condition input values (keyed by block id for conditional blocks, choice id for choices)
|
|
10
11
|
const [conditionInputs, setConditionInputs] = useState({});
|
|
11
12
|
const [debouncedConditionInputs, setDebouncedConditionInputs] = useState({});
|
|
@@ -311,11 +312,21 @@ export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, on
|
|
|
311
312
|
React.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" })),
|
|
312
313
|
node.type === 'npc' && (React.createElement(React.Fragment, null,
|
|
313
314
|
React.createElement("div", null,
|
|
314
|
-
React.createElement("label", { className: "text-[10px] text-
|
|
315
|
+
React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
|
|
316
|
+
React.createElement(CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
|
|
317
|
+
const character = characterId ? characters[characterId] : undefined;
|
|
318
|
+
onUpdate({
|
|
319
|
+
characterId,
|
|
320
|
+
speaker: character ? character.name : node.speaker, // Keep speaker as fallback
|
|
321
|
+
});
|
|
322
|
+
}, placeholder: "Select character...", className: "mb-2" }),
|
|
323
|
+
React.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
|
|
324
|
+
React.createElement("div", null,
|
|
325
|
+
React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
|
|
315
326
|
React.createElement("div", { className: "flex items-center gap-2" },
|
|
316
|
-
React.createElement("div", { className: "w-8 h-8 rounded-full bg-
|
|
317
|
-
React.createElement(User, { size: 16, className: "text-
|
|
318
|
-
React.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: "
|
|
327
|
+
React.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" },
|
|
328
|
+
React.createElement(User, { size: 16, className: "text-df-text-secondary" })),
|
|
329
|
+
React.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)" }))),
|
|
319
330
|
React.createElement("div", null,
|
|
320
331
|
React.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Content"),
|
|
321
332
|
React.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..." })),
|
|
@@ -375,14 +386,22 @@ export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, on
|
|
|
375
386
|
return (React.createElement("div", { key: block.id, className: `rounded p-2 space-y-2 ${styles.bg} ${styles.border} border-2` },
|
|
376
387
|
React.createElement("div", { className: "flex items-center gap-2" },
|
|
377
388
|
React.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'),
|
|
378
|
-
React.createElement("div", { className: "flex items-center gap-
|
|
379
|
-
React.createElement(
|
|
380
|
-
|
|
389
|
+
React.createElement("div", { className: "flex items-center gap-1.5 flex-1" },
|
|
390
|
+
React.createElement(CharacterSelector, { characters: characters, selectedCharacterId: block.characterId, onSelect: (characterId) => {
|
|
391
|
+
const newBlocks = [...node.conditionalBlocks];
|
|
392
|
+
const character = characterId ? characters[characterId] : undefined;
|
|
393
|
+
newBlocks[idx] = {
|
|
394
|
+
...block,
|
|
395
|
+
characterId,
|
|
396
|
+
speaker: character ? character.name : block.speaker, // Keep speaker as fallback
|
|
397
|
+
};
|
|
398
|
+
onUpdate({ conditionalBlocks: newBlocks });
|
|
399
|
+
}, placeholder: "Speaker...", compact: true, className: "flex-1" }),
|
|
381
400
|
React.createElement("input", { type: "text", value: block.speaker || '', onChange: (e) => {
|
|
382
401
|
const newBlocks = [...node.conditionalBlocks];
|
|
383
402
|
newBlocks[idx] = { ...block, speaker: e.target.value || undefined };
|
|
384
403
|
onUpdate({ conditionalBlocks: newBlocks });
|
|
385
|
-
}, className: `flex-1 bg-
|
|
404
|
+
}, 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" }))),
|
|
386
405
|
block.type !== 'else' && (() => {
|
|
387
406
|
const parseCondition = (conditionStr) => {
|
|
388
407
|
const conditions = [];
|
|
@@ -690,11 +709,21 @@ export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, on
|
|
|
690
709
|
}, className: "text-xs px-2 py-1 bg-[#12121a] border border-[#2a2a3e] rounded text-gray-400 hover:text-gray-200" }, "+ Add Else"))))) : (React.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."))))),
|
|
691
710
|
node.type === 'player' && (React.createElement("div", null,
|
|
692
711
|
React.createElement("div", null,
|
|
693
|
-
React.createElement("label", { className: "text-[10px] text-
|
|
712
|
+
React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
|
|
713
|
+
React.createElement(CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
|
|
714
|
+
const character = characterId ? characters[characterId] : undefined;
|
|
715
|
+
onUpdate({
|
|
716
|
+
characterId,
|
|
717
|
+
speaker: character ? character.name : node.speaker, // Keep speaker as fallback
|
|
718
|
+
});
|
|
719
|
+
}, placeholder: "Select character...", className: "mb-2" }),
|
|
720
|
+
React.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
|
|
721
|
+
React.createElement("div", null,
|
|
722
|
+
React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
|
|
694
723
|
React.createElement("div", { className: "flex items-center gap-2" },
|
|
695
|
-
React.createElement("div", { className: "w-8 h-8 rounded-full bg-
|
|
696
|
-
React.createElement(User, { size: 16, className: "text-
|
|
697
|
-
React.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-
|
|
724
|
+
React.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" },
|
|
725
|
+
React.createElement(User, { size: 16, className: "text-df-text-secondary" })),
|
|
726
|
+
React.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)" }))),
|
|
698
727
|
React.createElement("div", { className: "flex items-center justify-between mb-2 mt-4" },
|
|
699
728
|
React.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Choices"),
|
|
700
729
|
React.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;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
|
|
3
|
-
import { GitBranch, Play, Flag } from 'lucide-react';
|
|
3
|
+
import { GitBranch, Play, Flag, Hash } from 'lucide-react';
|
|
4
4
|
// Color scheme for choice edges (same as current implementation)
|
|
5
5
|
const CHOICE_COLORS = ['#e94560', '#8b5cf6', '#06b6d4', '#22c55e', '#f59e0b'];
|
|
6
6
|
export function PlayerNodeV2({ data, selected }) {
|
|
7
|
-
const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
7
|
+
const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
8
8
|
const choices = node.choices || [];
|
|
9
|
+
// Get character if characterId is set
|
|
10
|
+
const character = node.characterId ? characters[node.characterId] : undefined;
|
|
11
|
+
const displayName = character ? character.name : (node.speaker || 'Player');
|
|
12
|
+
const avatar = character?.avatar || '🎮';
|
|
9
13
|
const updateNodeInternals = useUpdateNodeInternals();
|
|
10
14
|
const headerRef = useRef(null);
|
|
11
15
|
const choiceRefs = useRef([]);
|
|
@@ -57,47 +61,62 @@ export function PlayerNodeV2({ data, selected }) {
|
|
|
57
61
|
: isEndNode
|
|
58
62
|
? 'border-df-end shadow-md'
|
|
59
63
|
: 'border-df-player-border';
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// Header background for player nodes
|
|
65
|
+
const headerBgClass = isStartNode
|
|
66
|
+
? 'bg-df-start-bg'
|
|
67
|
+
: isEndNode
|
|
68
|
+
? 'bg-df-end-bg'
|
|
69
|
+
: 'bg-df-player-header';
|
|
70
|
+
return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-player-selected/70' : ''} bg-df-player-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
|
|
67
71
|
React.createElement(Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
|
|
68
|
-
React.createElement("div", { ref: headerRef, className:
|
|
69
|
-
React.createElement(
|
|
70
|
-
React.createElement("
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
React.createElement("div", { ref: headerRef, className: `${headerBgClass} border-b-2 border-df-player-border px-3 py-2.5 flex items-center gap-3 relative` },
|
|
73
|
+
React.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),
|
|
74
|
+
React.createElement("div", { className: "flex-1 min-w-0" },
|
|
75
|
+
React.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
|
|
76
|
+
React.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
|
|
77
|
+
React.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}` },
|
|
78
|
+
React.createElement(Hash, { size: 12, className: "text-df-text-secondary" }),
|
|
79
|
+
React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
|
|
80
|
+
React.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" },
|
|
81
|
+
React.createElement(GitBranch, { size: 14, className: "text-df-player-selected" }),
|
|
82
|
+
React.createElement("span", { className: "text-[10px] font-semibold text-df-player-selected" }, "PLAYER"))),
|
|
83
|
+
isStartNode && (React.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" },
|
|
84
|
+
React.createElement(Play, { size: 8, fill: "currentColor" }),
|
|
85
|
+
" START")),
|
|
86
|
+
isEndNode && (React.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" },
|
|
87
|
+
React.createElement(Flag, { size: 8 }),
|
|
88
|
+
" END"))),
|
|
89
|
+
React.createElement("div", { className: "px-4 py-3 space-y-2" }, choices.map((choice, idx) => {
|
|
73
90
|
// Use calculated position or fallback
|
|
74
91
|
const choiceColor = CHOICE_COLORS[idx % CHOICE_COLORS.length];
|
|
75
92
|
return (React.createElement("div", { key: choice.id, ref: el => {
|
|
76
93
|
choiceRefs.current[idx] = el;
|
|
77
|
-
}, className: "
|
|
78
|
-
React.createElement("div", { className: "flex-
|
|
79
|
-
React.createElement("
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
flagType === '
|
|
88
|
-
flagType === '
|
|
89
|
-
flagType === '
|
|
90
|
-
flagType === '
|
|
91
|
-
flagType === '
|
|
92
|
-
flagType === '
|
|
93
|
-
'bg-df-flag-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
}, className: "relative group" },
|
|
95
|
+
React.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" },
|
|
96
|
+
React.createElement("div", { className: "flex-1 min-w-0" },
|
|
97
|
+
React.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
|
|
98
|
+
"\"",
|
|
99
|
+
choice.text || 'Empty choice',
|
|
100
|
+
"\""),
|
|
101
|
+
choice.setFlags && choice.setFlags.length > 0 && (React.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1" }, choice.setFlags.map(flagId => {
|
|
102
|
+
const flag = flagSchema?.flags.find(f => f.id === flagId);
|
|
103
|
+
const flagType = flag?.type || 'dialogue';
|
|
104
|
+
const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
|
|
105
|
+
flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
|
|
106
|
+
flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
|
|
107
|
+
flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
|
|
108
|
+
flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
|
|
109
|
+
flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
|
|
110
|
+
flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
|
|
111
|
+
'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
|
|
112
|
+
return (React.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]));
|
|
113
|
+
})))),
|
|
114
|
+
React.createElement(Handle, { type: "source", position: Position.Right, id: `choice-${idx}`, style: {
|
|
115
|
+
top: `50%`,
|
|
116
|
+
transform: `translateY(-50%)`,
|
|
117
|
+
right: '-6px',
|
|
118
|
+
borderColor: choiceColor,
|
|
119
|
+
}, className: "!bg-df-control-bg !border-2 hover:!border-df-player-selected hover:!bg-df-player-selected/20 !w-3 !h-3 !rounded-full" }))));
|
|
120
|
+
})),
|
|
121
|
+
React.createElement(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" })));
|
|
103
122
|
}
|
|
@@ -234,17 +234,17 @@ export function ReactFlowPOC({ dialogue }) {
|
|
|
234
234
|
const onNodesChange = useCallback((changes) => {
|
|
235
235
|
// Update dialogue tree positions
|
|
236
236
|
// This would sync back to our DialogueTree structure
|
|
237
|
-
|
|
237
|
+
// Handle node changes
|
|
238
238
|
}, []);
|
|
239
239
|
// Handle edge connections
|
|
240
240
|
const onConnect = useCallback((connection) => {
|
|
241
241
|
// Handle new edge connections
|
|
242
242
|
// This would update our DialogueTree structure
|
|
243
|
-
|
|
243
|
+
// Handle connection
|
|
244
244
|
}, []);
|
|
245
245
|
// Handle edge changes (delete, etc.)
|
|
246
246
|
const onEdgesChange = useCallback((changes) => {
|
|
247
|
-
|
|
247
|
+
// Handle edge changes
|
|
248
248
|
}, []);
|
|
249
249
|
return (React.createElement("div", { className: "w-full h-full" },
|
|
250
250
|
React.createElement("div", { className: "p-8 text-gray-400" },
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Characters
|
|
3
|
+
*
|
|
4
|
+
* Sample character data for use in examples and demos
|
|
5
|
+
*/
|
|
6
|
+
import type { Character } from '../types/characters';
|
|
7
|
+
export declare const exampleCharacters: Record<string, Character>;
|
|
8
|
+
/**
|
|
9
|
+
* Get all example characters
|
|
10
|
+
*/
|
|
11
|
+
export declare function getExampleCharacters(): Record<string, Character>;
|
|
12
|
+
/**
|
|
13
|
+
* Get a character by ID
|
|
14
|
+
*/
|
|
15
|
+
export declare function getExampleCharacter(id: string): Character | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* List all character IDs
|
|
18
|
+
*/
|
|
19
|
+
export declare function listExampleCharacterIds(): string[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Characters
|
|
3
|
+
*
|
|
4
|
+
* Sample character data for use in examples and demos
|
|
5
|
+
*/
|
|
6
|
+
export const exampleCharacters = {
|
|
7
|
+
'stranger': {
|
|
8
|
+
id: 'stranger',
|
|
9
|
+
name: 'Mysterious Stranger',
|
|
10
|
+
avatar: '👤',
|
|
11
|
+
description: 'A cloaked figure who appears at crossroads',
|
|
12
|
+
},
|
|
13
|
+
'bartender': {
|
|
14
|
+
id: 'bartender',
|
|
15
|
+
name: 'Bartender',
|
|
16
|
+
avatar: '🍺',
|
|
17
|
+
description: 'The friendly tavern keeper',
|
|
18
|
+
},
|
|
19
|
+
'merchant': {
|
|
20
|
+
id: 'merchant',
|
|
21
|
+
name: 'Merchant',
|
|
22
|
+
avatar: '💰',
|
|
23
|
+
description: 'A traveling trader',
|
|
24
|
+
},
|
|
25
|
+
'guard': {
|
|
26
|
+
id: 'guard',
|
|
27
|
+
name: 'City Guard',
|
|
28
|
+
avatar: '🛡️',
|
|
29
|
+
description: 'A vigilant city guard',
|
|
30
|
+
},
|
|
31
|
+
'wizard': {
|
|
32
|
+
id: 'wizard',
|
|
33
|
+
name: 'Wizard',
|
|
34
|
+
avatar: '🧙',
|
|
35
|
+
description: 'An ancient mage',
|
|
36
|
+
},
|
|
37
|
+
'player': {
|
|
38
|
+
id: 'player',
|
|
39
|
+
name: 'Player',
|
|
40
|
+
avatar: '🎮',
|
|
41
|
+
description: 'The player character',
|
|
42
|
+
},
|
|
43
|
+
'narrator': {
|
|
44
|
+
id: 'narrator',
|
|
45
|
+
name: 'Narrator',
|
|
46
|
+
avatar: '📖',
|
|
47
|
+
description: 'The story narrator',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Get all example characters
|
|
52
|
+
*/
|
|
53
|
+
export function getExampleCharacters() {
|
|
54
|
+
return exampleCharacters;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get a character by ID
|
|
58
|
+
*/
|
|
59
|
+
export function getExampleCharacter(id) {
|
|
60
|
+
return exampleCharacters[id];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* List all character IDs
|
|
64
|
+
*/
|
|
65
|
+
export function listExampleCharacterIds() {
|
|
66
|
+
return Object.keys(exampleCharacters);
|
|
67
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - index.ts: Public API for loading examples (this file)
|
|
12
12
|
*/
|
|
13
13
|
export { examplesRegistry, exampleFlagSchemas, getExampleMetadata, listExampleIds, getExampleFlagSchema, listFlagSchemaIds, type ExampleMetadata } from './examples-registry';
|
|
14
|
+
export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds, } from './example-characters';
|
|
14
15
|
/**
|
|
15
16
|
* Legacy exports for backward compatibility
|
|
16
17
|
* These maintain the old API while we migrate examples to Yarn format
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
export { examplesRegistry, exampleFlagSchemas, getExampleMetadata, listExampleIds, getExampleFlagSchema, listFlagSchemaIds } from './examples-registry';
|
|
15
15
|
import { getExampleDialogue as getYarnExampleDialogue } from './yarn-examples';
|
|
16
16
|
import { listExampleIds, listFlagSchemaIds, getExampleFlagSchema } from './examples-registry';
|
|
17
|
+
// Export character examples
|
|
18
|
+
export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds, } from './example-characters';
|
|
17
19
|
import { exampleDialogues as legacyExamples, demoFlagSchemas as legacySchemas } from './legacy-examples';
|
|
18
20
|
// Export legacy examples - these work alongside the new Yarn examples
|
|
19
21
|
export const exampleDialogues = legacyExamples;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -5,14 +5,17 @@ export { ScenePlayer as DialogueSimulator } from './components/ScenePlayer';
|
|
|
5
5
|
export { GuidePanel } from './components/GuidePanel';
|
|
6
6
|
export { FlagSelector } from './components/FlagSelector';
|
|
7
7
|
export { FlagManager } from './components/FlagManager';
|
|
8
|
+
export { CharacterSelector } from './components/CharacterSelector';
|
|
8
9
|
export { ZoomControls } from './components/ZoomControls';
|
|
9
10
|
export { ExampleLoader } from './components/ExampleLoader';
|
|
10
11
|
import './styles/scrollbar.css';
|
|
11
12
|
import './styles/theme.css';
|
|
12
13
|
export { exampleDialogues, demoFlagSchemas, getExampleDialogue, getDemoFlagSchema, listExamples, listDemoFlagSchemas } from './examples';
|
|
14
|
+
export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds } from './examples';
|
|
13
15
|
export * from './types';
|
|
14
16
|
export * from './types/flags';
|
|
15
17
|
export * from './types/game-state';
|
|
18
|
+
export * from './types/characters';
|
|
16
19
|
export * from './types/constants';
|
|
17
20
|
export { flattenGameState, validateGameState, extractFlagsFromGameState, type FlattenConfig, type FlattenedState } from './utils/game-state-flattener';
|
|
18
21
|
export { exportToYarn, importFromYarn } from './lib/yarn-converter';
|
package/dist/esm/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { ScenePlayer as DialogueSimulator } from './components/ScenePlayer';
|
|
|
5
5
|
export { GuidePanel } from './components/GuidePanel';
|
|
6
6
|
export { FlagSelector } from './components/FlagSelector';
|
|
7
7
|
export { FlagManager } from './components/FlagManager';
|
|
8
|
+
export { CharacterSelector } from './components/CharacterSelector';
|
|
8
9
|
export { ZoomControls } from './components/ZoomControls';
|
|
9
10
|
export { ExampleLoader } from './components/ExampleLoader';
|
|
10
11
|
// Export styles
|
|
@@ -12,10 +13,12 @@ import './styles/scrollbar.css';
|
|
|
12
13
|
import './styles/theme.css';
|
|
13
14
|
// Export examples
|
|
14
15
|
export { exampleDialogues, demoFlagSchemas, getExampleDialogue, getDemoFlagSchema, listExamples, listDemoFlagSchemas } from './examples';
|
|
16
|
+
export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds } from './examples';
|
|
15
17
|
// Export all types
|
|
16
18
|
export * from './types';
|
|
17
19
|
export * from './types/flags';
|
|
18
20
|
export * from './types/game-state';
|
|
21
|
+
export * from './types/characters';
|
|
19
22
|
export * from './types/constants';
|
|
20
23
|
// Export game state utilities
|
|
21
24
|
export { flattenGameState, validateGameState, extractFlagsFromGameState } from './utils/game-state-flattener';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character Types
|
|
3
|
+
*
|
|
4
|
+
* Defines character data structure for dialogue nodes
|
|
5
|
+
*/
|
|
6
|
+
export interface Character {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
avatar?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
export interface CharactersState {
|
|
14
|
+
[characterId: string]: Character;
|
|
15
|
+
}
|
|
@@ -19,12 +19,14 @@ export interface FlagState {
|
|
|
19
19
|
* Legacy alias for backward compatibility
|
|
20
20
|
*/
|
|
21
21
|
export type GameFlagState = FlagState;
|
|
22
|
+
import type { Character } from './characters';
|
|
22
23
|
/**
|
|
23
24
|
* Base game state structure that users can extend
|
|
24
25
|
* Must have a 'flags' property, but can have any other structure
|
|
25
26
|
*/
|
|
26
27
|
export interface BaseGameState {
|
|
27
28
|
flags?: FlagState;
|
|
29
|
+
characters?: Record<string, Character>;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Convenience type for extending game state
|
|
@@ -20,12 +20,14 @@ export interface ConditionalBlock {
|
|
|
20
20
|
condition?: Condition[];
|
|
21
21
|
content: string;
|
|
22
22
|
speaker?: string;
|
|
23
|
+
characterId?: string;
|
|
23
24
|
nextNodeId?: string;
|
|
24
25
|
}
|
|
25
26
|
export interface DialogueNode {
|
|
26
27
|
id: string;
|
|
27
28
|
type: NodeType;
|
|
28
29
|
speaker?: string;
|
|
30
|
+
characterId?: string;
|
|
29
31
|
content: string;
|
|
30
32
|
choices?: Choice[];
|
|
31
33
|
nextNodeId?: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Characters
|
|
3
|
+
*
|
|
4
|
+
* Sample character data for use in examples and demos
|
|
5
|
+
*/
|
|
6
|
+
import type { Character } from '../types/characters';
|
|
7
|
+
export declare const exampleCharacters: Record<string, Character>;
|
|
8
|
+
/**
|
|
9
|
+
* Get all example characters
|
|
10
|
+
*/
|
|
11
|
+
export declare function getExampleCharacters(): Record<string, Character>;
|
|
12
|
+
/**
|
|
13
|
+
* Get a character by ID
|
|
14
|
+
*/
|
|
15
|
+
export declare function getExampleCharacter(id: string): Character | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* List all character IDs
|
|
18
|
+
*/
|
|
19
|
+
export declare function listExampleCharacterIds(): string[];
|