import { ref, computed, shallowRef, triggerRef } from 'vue' import type { GameData, SceneNode, Choice } from '@engine/types' export function useGraphEditor() { const gameData = shallowRef({ scenes: {}, startScene: '', variables: {} }) const selectedNodeId = ref(null) const startSceneId = ref('') const sceneList = computed(() => Object.values(gameData.value.scenes).map((s) => ({ id: s.id, label: s.id })), ) const sceneNodes = computed(() => Object.values(gameData.value.scenes).map((s) => ({ id: s.id, label: s.id })), ) const sceneEdges = computed(() => { const result: { id: string; source: string; target: string; label?: string }[] = [] for (const [id, scene] of Object.entries(gameData.value.scenes)) { if (scene.choices) { let ci = 0 for (const c of scene.choices) { if (c.targetScene) { result.push({ id: `${id}_choice_${ci}`, source: id, target: c.targetScene, label: c.text.slice(0, 10) }) } ci++ } } if (scene.nextScene) { result.push({ id: `${id}_next`, source: id, target: scene.nextScene, label: '\u2192 \u9ed8\u8ba4' }) } if (scene.qte) { if (scene.qte.successScene) result.push({ id: `${id}_qte_s`, source: id, target: scene.qte.successScene, label: 'QTE\u6210\u529f' }) if (scene.qte.failScene) result.push({ id: `${id}_qte_f`, source: id, target: scene.qte.failScene, label: 'QTE\u5931\u8d25' }) } } return result }) const selectedScene = computed(() => { if (!selectedNodeId.value) return null return gameData.value.scenes[selectedNodeId.value] ?? null }) function trigger() { triggerRef(gameData) } function loadJSON(json: GameData) { gameData.value = JSON.parse(JSON.stringify(json)) trigger() startSceneId.value = json.startScene || '' selectedNodeId.value = null } function exportJSON(): GameData { return JSON.parse(JSON.stringify({ ...gameData.value, startScene: startSceneId.value })) } function generateId(): string { let i = Object.keys(gameData.value.scenes).length + 1 while (gameData.value.scenes[`scene_${i}`]) i++ return `scene_${i}` } function addScene(): string { const id = generateId() gameData.value = { ...gameData.value, scenes: { ...gameData.value.scenes, [id]: { id, videoUrl: '', choices: [], nextScene: '', subtitleUrl: '', onEnter: [], }, }, } trigger() return id } function deleteScene(id: string) { if (startSceneId.value === id) return const nextScenes = { ...gameData.value.scenes } delete nextScenes[id] for (const key of Object.keys(nextScenes)) { const s = nextScenes[key] if (s.choices) nextScenes[key] = { ...s, choices: s.choices.filter((c) => c.targetScene !== id) } if (s.nextScene === id) nextScenes[key] = { ...nextScenes[key], nextScene: '' } if (s.qte) { const qte = { ...s.qte } let changed = false if (qte.successScene === id) { qte.successScene = ''; changed = true } if (qte.failScene === id) { qte.failScene = ''; changed = true } if (changed) nextScenes[key] = { ...nextScenes[key], qte } } } gameData.value = { ...gameData.value, scenes: nextScenes } trigger() if (selectedNodeId.value === id) selectedNodeId.value = null } function updateScene(id: string, partial: Partial) { const scene = gameData.value.scenes[id] if (!scene) return gameData.value = { ...gameData.value, scenes: { ...gameData.value.scenes, [id]: { ...scene, ...partial } }, } trigger() } function addChoice(sourceId: string) { const scene = gameData.value.scenes[sourceId] if (!scene) return gameData.value = { ...gameData.value, scenes: { ...gameData.value.scenes, [sourceId]: { ...scene, choices: [...(scene.choices || []), { text: '\u65b0\u9009\u9879', targetScene: '' }], }, }, } trigger() } function updateChoice(sourceId: string, index: number, partial: Partial) { const scene = gameData.value.scenes[sourceId] if (!scene?.choices) return const newChoices = scene.choices.map((c, i) => (i === index ? { ...c, ...partial } : c)) gameData.value = { ...gameData.value, scenes: { ...gameData.value.scenes, [sourceId]: { ...scene, choices: newChoices } }, } trigger() } function deleteChoice(sourceId: string, index: number) { const scene = gameData.value.scenes[sourceId] if (!scene?.choices) return gameData.value = { ...gameData.value, scenes: { ...gameData.value.scenes, [sourceId]: { ...scene, choices: scene.choices.filter((_, i) => i !== index) }, }, } trigger() } return { gameData, selectedNodeId, selectedScene, sceneList, sceneNodes, sceneEdges, startSceneId, loadJSON, exportJSON, addScene, deleteScene, updateScene, addChoice, updateChoice, deleteChoice, generateId, } }