105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
import { useDialogueStore } from '../../store/dialogueStore';
|
|
import { makeNodeId } from '../../utils/idGen';
|
|
import { DialogueNode, NodeType } from '../../types/dialogue';
|
|
import { MAIN_CHARACTER } from '../../constants/characters';
|
|
import styles from './GraphToolbar.module.css';
|
|
|
|
const NODE_DEFAULTS: Record<string, () => Omit<DialogueNode, 'id'>> = {
|
|
Line: () => ({
|
|
type: 'Line',
|
|
speaker: MAIN_CHARACTER,
|
|
portrait: 'resources/dialogue/portrait_hero_neutral.png',
|
|
text: '',
|
|
next: '',
|
|
}),
|
|
Choice: () => ({
|
|
type: 'Choice',
|
|
speaker: MAIN_CHARACTER,
|
|
portrait: 'resources/dialogue/portrait_hero_neutral.png',
|
|
text: '',
|
|
choices: [],
|
|
}),
|
|
Condition: () => ({
|
|
type: 'Condition',
|
|
conditions: [],
|
|
trueNext: '',
|
|
falseNext: '',
|
|
}),
|
|
SetFlag: () => ({
|
|
type: 'SetFlag',
|
|
effects: [],
|
|
next: '',
|
|
}),
|
|
CutsceneStart: () => ({
|
|
type: 'CutsceneStart',
|
|
cutsceneId: '',
|
|
next: '',
|
|
}),
|
|
End: () => ({ type: 'End' }),
|
|
};
|
|
|
|
export function GraphToolbar() {
|
|
const { file, selectedDialogueId, selectedNodeId, addNode, applyAutoLayout, startPlay, setDialogueMobileMode, persistentFlags, resetFlags } = useDialogueStore();
|
|
|
|
const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
|
|
if (!dialogue) return null;
|
|
|
|
function handleAddNode(type: NodeType) {
|
|
if (!selectedDialogueId || !dialogue) return;
|
|
const existingIds = new Set(dialogue.nodes.map(n => n.id));
|
|
const id = makeNodeId(type, existingIds);
|
|
const node = { id, ...NODE_DEFAULTS[type]() } as DialogueNode;
|
|
addNode(selectedDialogueId, node, selectedNodeId ?? undefined);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.toolbar}>
|
|
<div className={styles.group}>
|
|
<span className={styles.label}>Add:</span>
|
|
{(['Line', 'Choice', 'Condition', 'SetFlag', 'CutsceneStart', 'End'] as NodeType[]).map(type => (
|
|
<button key={type} className={styles.btn} onClick={() => handleAddNode(type)}>
|
|
{type}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className={styles.separator} />
|
|
<div className={styles.group}>
|
|
<button className={styles.btn} onClick={() => applyAutoLayout(selectedDialogueId!)}>
|
|
⬡ Auto Layout
|
|
</button>
|
|
<button className={[styles.btn, styles.btnPlay].join(' ')} onClick={startPlay}>
|
|
▶ Play
|
|
</button>
|
|
</div>
|
|
<div className={styles.separator} />
|
|
<div className={styles.group}>
|
|
<label className={styles.toggle}>
|
|
<input
|
|
type="checkbox"
|
|
checked={!!dialogue.mobileMode}
|
|
onChange={e => setDialogueMobileMode(selectedDialogueId!, e.target.checked)}
|
|
/>
|
|
<span>📱 Mobile</span>
|
|
</label>
|
|
</div>
|
|
{dialogue.mobileMode && (
|
|
<div className={styles.mobileBanner}>
|
|
Mobile mode — portraits will be overridden on export
|
|
</div>
|
|
)}
|
|
{Object.keys(persistentFlags).length > 0 && (
|
|
<>
|
|
<div className={styles.separator} />
|
|
<div className={styles.group}>
|
|
<span className={styles.label}>🚩 Flags:</span>
|
|
{Object.entries(persistentFlags).map(([k, v]) => (
|
|
<span key={k} className={styles.flagPill}>{k}={v}</span>
|
|
))}
|
|
<button className={styles.btnReset} onClick={resetFlags}>Reset</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|