working on cutscenes
This commit is contained in:
1 parent
54cc118df7
commit
6349859e66
47 files changed
+6234
-21
No files matched your search
@@ -0,0 +1,242 @@
|
||||
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<Omit<Cutscene, 'imageSegments' | 'lines'>>) => 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<ImageSegment>) => void;
|
||||
updateLayerFrom: (index: number, patch: Partial<ImagePose>) => void;
|
||||
updateLayerTo: (index: number, patch: Partial<ImagePose>) => void;
|
||||
|
||||
// Subtitle lines
|
||||
addLine: () => void;
|
||||
removeLine: (index: number) => void;
|
||||
moveLineUp: (index: number) => void;
|
||||
moveLineDown: (index: number) => void;
|
||||
updateLine: (index: number, patch: Partial<SubtitleLine>) => 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<CutsceneStore>()(
|
||||
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
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user