Files
tianshu-engine/src/composables/useGameEngine.ts

185 lines
4.8 KiB
TypeScript

import { onUnmounted } from 'vue'
import { Engine } from '@engine/core/Engine'
import { SaveSystem } from '@engine/systems/SaveSystem'
import type { GameData } from '@engine/types'
import { useGameStore } from '@/stores/gameStore'
export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVideoElement | null]) {
const engine = new Engine()
const saveSystem = new SaveSystem()
const store = useGameStore()
let lastThumbnail: string | undefined
if (import.meta.env.DEV) {
;(window as any).__sm = engine.stateManager
;(window as any).__store = store
}
async function loadGame(dataUrl: string) {
const resp = await fetch(dataUrl)
const data: GameData = await resp.json()
engine.sceneManager.load(data)
engine.stateManager.init(data.variables)
store.setChapters(data.chapters || [])
const unlocked = await saveSystem.getUnlockedChapters()
store.setUnlockedChapters(unlocked)
}
function registerEvents() {
engine.setChapterUnlockHandler(async (chapterId) => {
await saveSystem.unlockChapter(chapterId)
store.addUnlockedChapter(chapterId)
})
engine.on('sceneChange', (scene) => {
store.setScene(scene)
store.clearChoices()
store.clearTimer()
store.clearHotspots()
store.setIsImageScene(scene.type === 'image')
saveGame(0)
})
engine.on('choiceRequest', (choiceList) => {
store.setChoices(choiceList)
})
engine.on('choiceTimer', (timerState) => {
store.setTimer(timerState.total, timerState.remaining)
})
engine.on('choiceTimeout', () => {
store.clearChoices()
store.clearTimer()
})
engine.on('hotspotRequest', (list) => {
store.setHotspots(list)
})
engine.on('hotspotUpdate', (list) => {
store.setHotspots(list)
})
engine.on('videoEnd', () => {
try {
const video = engine.videoManager.getActiveVideoElement()
if (video && video.readyState >= 2) {
const canvas = document.createElement('canvas')
canvas.width = 320
canvas.height = 180
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(video, 0, 0, 320, 180)
lastThumbnail = canvas.toDataURL('image/jpeg', 0.6)
}
}
} catch { /* ignore */ }
})
engine.on('gameEnd', () => {
store.setGameEnded(true)
engine.choiceSystem.stop()
})
engine.on('qteTrigger', (qte) => {
store.showQTE(qte)
})
engine.on('qteTimer', ({ remaining }) => {
store.updateQTE(remaining)
})
engine.on('qteResult', ({ success }) => {
store.resolveQTE(success)
})
engine.videoManager.onTimeUpdate((t: number) => {
store.setVideoTime(t)
})
}
function start() {
const [elA, elB] = videoEls()
if (elA && elB) engine.videoManager.attach(elA, elB)
registerEvents()
engine.start()
}
async function resumeAutoSave(): Promise<boolean> {
const [elA, elB] = videoEls()
if (elA && elB) engine.videoManager.attach(elA, elB)
registerEvents()
return await loadGameFromSlot(0)
}
function makeChoice(index: number) {
const scene = store.currentScene
if (!scene?.choices) return
engine.choiceSystem.stop()
store.clearTimer()
engine.makeChoice(scene.choices[index])
}
function clickHotspot(hotspotId: string) {
const scene = store.currentScene
if (!scene?.hotspots) return
const hs = scene.hotspots.find((h) => h.id === hotspotId)
if (hs) {
engine.clickHotspot(hs)
}
}
function startChapter(chapterId: string) {
const [elA, elB] = videoEls()
if (elA && elB) engine.videoManager.attach(elA, elB)
store.setGameEnded(false)
engine.startChapter(chapterId)
}
async function saveGame(slot: number) {
const state = engine.stateManager
const currentScene = store.currentScene
await saveSystem.save(slot, {
timestamp: Date.now(),
currentScene: currentScene?.id ?? '',
variables: state.variables,
flags: [...state.flags],
history: state.history,
thumbnail: lastThumbnail,
})
await refreshSaves()
}
async function loadGameFromSlot(slot: number): Promise<boolean> {
const data = await saveSystem.load(slot)
if (!data) return false
store.setGameEnded(false)
engine.resumeScene(data.currentScene, {
variables: data.variables,
flags: data.flags,
history: data.history,
})
return true
}
async function refreshSaves() {
const list = await saveSystem.listSlots()
store.setSaves(list)
}
function destroy() {
engine.destroy()
}
onUnmounted(() => {
destroy()
})
return { loadGame, start, resumeAutoSave, makeChoice, clickHotspot, startChapter,
saveGame, loadGameFromSlot, refreshSaves, engine, saveSystem }
}