refactor: rewrite editor with immutable state, async-safe Vue Flow, and loading guard
This commit is contained in:
@@ -1,37 +1,58 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, shallowRef, triggerRef } from 'vue'
|
||||
import type { GameData, SceneNode, Choice } from '@engine/types'
|
||||
|
||||
export interface EditorNode {
|
||||
id: string
|
||||
label: string
|
||||
videoUrl: string
|
||||
subtitleUrl: string
|
||||
choices: Choice[]
|
||||
nextScene: string
|
||||
onEnter: any[]
|
||||
qte: any | null
|
||||
}
|
||||
|
||||
export function useGraphEditor() {
|
||||
const gameData = ref<GameData>({ scenes: {}, startScene: '', variables: {} })
|
||||
const gameData = shallowRef<GameData>({ scenes: {}, startScene: '', variables: {} })
|
||||
const selectedNodeId = ref<string | null>(null)
|
||||
const startSceneId = ref('')
|
||||
|
||||
const selectedNode = computed(() => {
|
||||
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
|
||||
})
|
||||
|
||||
const sceneList = computed(() => {
|
||||
return Object.values(gameData.value.scenes).map((s) => ({
|
||||
id: s.id,
|
||||
label: s.id,
|
||||
}))
|
||||
})
|
||||
function trigger() {
|
||||
triggerRef(gameData)
|
||||
}
|
||||
|
||||
function loadJSON(json: GameData) {
|
||||
gameData.value = JSON.parse(JSON.stringify(json))
|
||||
startSceneId.value = json.startScene
|
||||
trigger()
|
||||
startSceneId.value = json.startScene || ''
|
||||
selectedNodeId.value = null
|
||||
}
|
||||
|
||||
function exportJSON(): GameData {
|
||||
@@ -46,76 +67,112 @@ export function useGraphEditor() {
|
||||
|
||||
function addScene(): string {
|
||||
const id = generateId()
|
||||
gameData.value.scenes[id] = {
|
||||
id,
|
||||
videoUrl: '',
|
||||
choices: [],
|
||||
nextScene: '',
|
||||
subtitleUrl: '',
|
||||
onEnter: [],
|
||||
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
|
||||
delete gameData.value.scenes[id]
|
||||
for (const s of Object.values(gameData.value.scenes)) {
|
||||
s.choices = (s.choices || []).filter((c) => c.targetScene !== id)
|
||||
if (s.nextScene === id) s.nextScene = ''
|
||||
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<EditorNode>) {
|
||||
function updateScene(id: string, partial: Partial<SceneNode>) {
|
||||
const scene = gameData.value.scenes[id]
|
||||
if (!scene) return
|
||||
Object.assign(scene, partial)
|
||||
gameData.value.scenes = { ...gameData.value.scenes }
|
||||
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
|
||||
if (!scene.choices) scene.choices = []
|
||||
scene.choices.push({
|
||||
text: '新选项',
|
||||
targetScene: '',
|
||||
})
|
||||
gameData.value.scenes = { ...gameData.value.scenes }
|
||||
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<Choice>) {
|
||||
const scene = gameData.value.scenes[sourceId]
|
||||
if (!scene?.choices) return
|
||||
Object.assign(scene.choices[index], partial)
|
||||
gameData.value.scenes = { ...gameData.value.scenes }
|
||||
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
|
||||
scene.choices.splice(index, 1)
|
||||
gameData.value.scenes = { ...gameData.value.scenes }
|
||||
}
|
||||
|
||||
function newSceneData(): EditorNode {
|
||||
return {
|
||||
id: '',
|
||||
label: '',
|
||||
videoUrl: '',
|
||||
subtitleUrl: '',
|
||||
choices: [],
|
||||
nextScene: '',
|
||||
onEnter: [],
|
||||
qte: null,
|
||||
gameData.value = {
|
||||
...gameData.value,
|
||||
scenes: {
|
||||
...gameData.value.scenes,
|
||||
[sourceId]: { ...scene, choices: scene.choices.filter((_, i) => i !== index) },
|
||||
},
|
||||
}
|
||||
trigger()
|
||||
}
|
||||
|
||||
return {
|
||||
gameData, selectedNodeId, selectedNode, sceneList, startSceneId,
|
||||
loadJSON, exportJSON, addScene, deleteScene, updateScene,
|
||||
addChoice, updateChoice, deleteChoice,
|
||||
newSceneData, generateId,
|
||||
gameData,
|
||||
selectedNodeId,
|
||||
selectedScene,
|
||||
sceneList,
|
||||
sceneNodes,
|
||||
sceneEdges,
|
||||
startSceneId,
|
||||
loadJSON,
|
||||
exportJSON,
|
||||
addScene,
|
||||
deleteScene,
|
||||
updateScene,
|
||||
addChoice,
|
||||
updateChoice,
|
||||
deleteChoice,
|
||||
generateId,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user