- editor/: stand-alone Vite multi-page app for visual scenario editing - editor/components/SceneGraph.vue: Vue Flow graph with scene nodes, branch/default/QTE edges - editor/components/NodeEditor.vue: right panel editing video/subtitle paths, choices, QTE params - editor/components/PreviewPanel.vue: embedded video player previewing selected scene - editor/composables/useGraphEditor.ts: bidirectional graph<->JSON sync - editor/App.vue: toolbar (new scene, import/export JSON, load demo, start scene selector) - @vue-flow/core|background|controls: graph visualization dependencies - vite.config.ts: multi-page build (main + editor) - ROADMAP: mark P3 as completed
122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
import { ref, computed } 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 selectedNodeId = ref<string | null>(null)
|
|
const startSceneId = ref('')
|
|
|
|
const selectedNode = 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 loadJSON(json: GameData) {
|
|
gameData.value = JSON.parse(JSON.stringify(json))
|
|
startSceneId.value = json.startScene
|
|
}
|
|
|
|
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.scenes[id] = {
|
|
id,
|
|
videoUrl: '',
|
|
choices: [],
|
|
nextScene: '',
|
|
subtitleUrl: '',
|
|
onEnter: [],
|
|
}
|
|
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 = ''
|
|
}
|
|
if (selectedNodeId.value === id) selectedNodeId.value = null
|
|
}
|
|
|
|
function updateScene(id: string, partial: Partial<EditorNode>) {
|
|
const scene = gameData.value.scenes[id]
|
|
if (!scene) return
|
|
Object.assign(scene, partial)
|
|
gameData.value.scenes = { ...gameData.value.scenes }
|
|
}
|
|
|
|
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 }
|
|
}
|
|
|
|
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 }
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
return {
|
|
gameData, selectedNodeId, selectedNode, sceneList, startSceneId,
|
|
loadJSON, exportJSON, addScene, deleteScene, updateScene,
|
|
addChoice, updateChoice, deleteChoice,
|
|
newSceneData, generateId,
|
|
}
|
|
}
|