import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; import type { CutsceneFile, Cutscene, ImageSegment, SubtitleLine, ImagePose } from '../types/cutscene'; export type PlayState = 'stopped' | 'playing' | 'paused'; interface CutsceneStore { file: CutsceneFile | null; selectedCutsceneId: string | null; selectedLayerIndex: number | null; playState: PlayState; currentTimeMs: number; // File I/O loadFile: (file: CutsceneFile) => void; getExportData: () => CutsceneFile | null; // Cutscene CRUD addCutscene: () => void; deleteCutscene: (id: string) => void; selectCutscene: (id: string | null) => void; updateCutscene: (id: string, patch: Partial>) => void; // Layer CRUD selectLayer: (index: number | null) => void; addLayer: () => void; removeLayer: (index: number) => void; moveLayerUp: (index: number) => void; moveLayerDown: (index: number) => void; updateLayer: (index: number, patch: Partial) => void; updateLayerFrom: (index: number, patch: Partial) => void; updateLayerTo: (index: number, patch: Partial) => void; // Subtitle lines addLine: () => void; removeLine: (index: number) => void; moveLineUp: (index: number) => void; moveLineDown: (index: number) => void; updateLine: (index: number, patch: Partial) => void; // Playback setPlayState: (state: PlayState) => void; setCurrentTime: (ms: number) => void; } function newCutscene(id: string): Cutscene { return { id, skippable: true, durationMs: 5000, fadeOutMs: 500, fadeInMs: 500, endFadeOutMs: 500, endFadeInMs: 500, onFadeInCallback: '', imageSegments: [], lines: [], }; } function newSegment(): ImageSegment { return { path: '', width: 1280, height: 720, startMs: 0, endMs: 5000, fadeInMs: 0, fadeOutMs: 0, easing: 'Linear', from: { centerX: 0.5, centerY: 0.5, scale: 1.0 }, to: { centerX: 0.5, centerY: 0.5, scale: 1.0 }, }; } function newLine(): SubtitleLine { return { speaker: '', text: '', durationMs: 3000, waitForConfirm: false, luaCallback: '', }; } function getSelected(file: CutsceneFile | null, id: string | null): Cutscene | null { if (!file || !id) return null; return file.cutscenes.find(c => c.id === id) ?? null; } export const useCutsceneStore = create()( immer((set, get) => ({ file: null, selectedCutsceneId: null, selectedLayerIndex: null, playState: 'stopped', currentTimeMs: 0, loadFile: (file) => set(s => { s.file = file; s.selectedCutsceneId = file.cutscenes[0]?.id ?? null; s.selectedLayerIndex = null; s.playState = 'stopped'; s.currentTimeMs = 0; }), getExportData: () => get().file, addCutscene: () => set(s => { if (!s.file) s.file = { cutscenes: [] }; let base = 'cutscene_new'; let n = 1; const ids = new Set(s.file.cutscenes.map(c => c.id)); while (ids.has(`${base}_${n}`)) n++; const id = `${base}_${n}`; s.file.cutscenes.push(newCutscene(id)); s.selectedCutsceneId = id; s.selectedLayerIndex = null; }), deleteCutscene: (id) => set(s => { if (!s.file) return; const idx = s.file.cutscenes.findIndex(c => c.id === id); if (idx === -1) return; s.file.cutscenes.splice(idx, 1); if (s.selectedCutsceneId === id) { s.selectedCutsceneId = s.file.cutscenes[0]?.id ?? null; s.selectedLayerIndex = null; } }), selectCutscene: (id) => set(s => { s.selectedCutsceneId = id; s.selectedLayerIndex = null; s.playState = 'stopped'; s.currentTimeMs = 0; }), updateCutscene: (id, patch) => set(s => { if (!s.file) return; const c = s.file.cutscenes.find(c => c.id === id); if (!c) return; Object.assign(c, patch); }), selectLayer: (index) => set(s => { s.selectedLayerIndex = index; }), addLayer: () => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; cutscene.imageSegments.push(newSegment()); s.selectedLayerIndex = cutscene.imageSegments.length - 1; }), removeLayer: (index) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; cutscene.imageSegments.splice(index, 1); if (s.selectedLayerIndex === index) s.selectedLayerIndex = null; else if (s.selectedLayerIndex !== null && s.selectedLayerIndex > index) s.selectedLayerIndex--; }), moveLayerUp: (index) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene || index <= 0) return; const segs = cutscene.imageSegments; [segs[index - 1], segs[index]] = [segs[index], segs[index - 1]]; if (s.selectedLayerIndex === index) s.selectedLayerIndex = index - 1; else if (s.selectedLayerIndex === index - 1) s.selectedLayerIndex = index; }), moveLayerDown: (index) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; const segs = cutscene.imageSegments; if (index >= segs.length - 1) return; [segs[index], segs[index + 1]] = [segs[index + 1], segs[index]]; if (s.selectedLayerIndex === index) s.selectedLayerIndex = index + 1; else if (s.selectedLayerIndex === index + 1) s.selectedLayerIndex = index; }), updateLayer: (index, patch) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; Object.assign(cutscene.imageSegments[index], patch); }), updateLayerFrom: (index, patch) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; Object.assign(cutscene.imageSegments[index].from, patch); }), updateLayerTo: (index, patch) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; Object.assign(cutscene.imageSegments[index].to, patch); }), addLine: () => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; cutscene.lines.push(newLine()); }), removeLine: (index) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; cutscene.lines.splice(index, 1); }), moveLineUp: (index) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene || index <= 0) return; const lines = cutscene.lines; [lines[index - 1], lines[index]] = [lines[index], lines[index - 1]]; }), moveLineDown: (index) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; const lines = cutscene.lines; if (index >= lines.length - 1) return; [lines[index], lines[index + 1]] = [lines[index + 1], lines[index]]; }), updateLine: (index, patch) => set(s => { const cutscene = getSelected(s.file, s.selectedCutsceneId); if (!cutscene) return; Object.assign(cutscene.lines[index], patch); }), setPlayState: (state) => set(s => { s.playState = state; }), setCurrentTime: (ms) => set(s => { s.currentTimeMs = ms; }), })) ); export function useSelectedCutscene() { return useCutsceneStore(s => s.file?.cutscenes.find(c => c.id === s.selectedCutsceneId) ?? null ); }