diff --git a/editor/App.vue b/editor/App.vue
index d9cd484..081b5ab 100644
--- a/editor/App.vue
+++ b/editor/App.vue
@@ -1,5 +1,5 @@
diff --git a/editor/composables/useGraphEditor.ts b/editor/composables/useGraphEditor.ts
index 967a4a6..aeec4a9 100644
--- a/editor/composables/useGraphEditor.ts
+++ b/editor/composables/useGraphEditor.ts
@@ -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({ scenes: {}, startScene: '', variables: {} })
+ const gameData = shallowRef({ scenes: {}, startScene: '', variables: {} })
const selectedNodeId = ref(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) {
+ function updateScene(id: string, partial: Partial) {
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) {
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,
}
}