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' import { useI18n } from '@/composables/useI18n' export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVideoElement | null]) { const engine = new Engine() const saveSystem = new SaveSystem() const store = useGameStore() const { t } = useI18n() let lastThumbnail: string | undefined if (import.meta.env.DEV) { ;(window as any).__sm = engine.stateManager ;(window as any).__store = store } engine.setChapterUnlockHandler(async (chapterId) => { await saveSystem.unlockChapter(chapterId) store.addUnlockedChapter(chapterId) }) engine.setMarkWatchedHandler(async (sceneId) => { await saveSystem.markWatched(sceneId) }) engine.setMarkVisitedHandler(async (sceneId) => { store.addVisitedSceneId(sceneId) await saveSystem.markVisited(sceneId) }) engine.achievementSystem.setUnlockCallback(async (ach) => { await saveSystem.unlockAchievement(ach.id) store.addUnlockedAchievement(ach.id) }) engine.on('sceneChange', (scene) => { store.setScene(scene) store.clearChoices() store.clearTimer() store.clearHotspots() store.setIsImageScene(scene.type === 'image') saveGame(0) }) engine.on('choiceRequest', (choiceList) => { const translated = choiceList.map((c: any) => ({ ...c, text: c.textKey ? t(c.textKey) : c.text, })) store.setChoices(translated) }) 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.clearQTE() store.setGameEnded(true) engine.choiceSystem.stop() }) engine.on('qteTrigger', (qte) => { store.showQTE(qte) }) engine.on('qteTimer', ({ remaining, total }) => { store.qteTotal = total store.updateQTE(remaining) }) engine.on('qteResult', ({ success }) => { store.resolveQTE(success) }) engine.videoManager.onTimeUpdate((t: number) => { store.setVideoTime(t) }) function resolveAsset(base: string, path: string): string { if (!path || path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) return path const b = base.endsWith('/') ? base.slice(0, -1) : base const p = path.startsWith('/') ? path : '/' + path return b + p } function applyAssetBase(data: GameData) { const base = data.assetBase || '' if (!base) return for (const scene of Object.values(data.scenes)) { if (scene.videoUrl) scene.videoUrl = resolveAsset(base, scene.videoUrl) if (scene.subtitleUrl) scene.subtitleUrl = resolveAsset(base, scene.subtitleUrl) if (scene.imageUrl) scene.imageUrl = resolveAsset(base, scene.imageUrl) if (scene.bgmUrl) scene.bgmUrl = resolveAsset(base, scene.bgmUrl) if (scene.thumbnail) scene.thumbnail = resolveAsset(base, scene.thumbnail) if (scene.subtitles) { for (const k of Object.keys(scene.subtitles)) { scene.subtitles[k] = resolveAsset(base, scene.subtitles[k]) } } } if (data.endings) { for (const e of data.endings) { if (e.thumbnail) e.thumbnail = resolveAsset(base, e.thumbnail) } } if (data.chapters) { for (const c of data.chapters) { if (c.thumbnail) c.thumbnail = resolveAsset(base, c.thumbnail) } } if (data.introVideo) data.introVideo = resolveAsset(base, data.introVideo) if (data.menuVideo) data.menuVideo = resolveAsset(base, data.menuVideo) } async function loadGame(dataUrl: string) { const resp = await fetch(dataUrl) const data: GameData = await resp.json() applyAssetBase(data) engine.sceneManager.load(data) engine.stateManager.init(data.variables) store.setChapters(data.chapters || []) store.setAchievementDefs(data.achievements || []) const unlocked = await saveSystem.getUnlockedChapters() store.setUnlockedChapters(unlocked) const achieved = await saveSystem.getUnlockedAchievements() store.setUnlockedAchievementIds(achieved) engine.achievementSystem.init(data.achievements || [], achieved) store.setEndings(data.endings || []) store.setStoryLocales(data.locales, data.assetBase) const visitedIds = await saveSystem.getVisitedSceneIds() store.setVisitedSceneIds(visitedIds) store.setIntroVideo(data.introVideo || '') store.setMenuVideo(data.menuVideo || '') } function ensureVideo() { const [elA, elB] = videoEls() if (elA && elB) engine.videoManager.attach(elA, elB) } function start() { ensureVideo() engine.start() } async function resumeAutoSave(): Promise { ensureVideo() store.setGameEnded(false) return await loadGameFromSlot(0) } function startChapter(chapterId: string) { const [elA, elB] = videoEls() if (elA && elB) engine.videoManager.attach(elA, elB) store.setGameEnded(false) engine.startChapter(chapterId) } function startAtScene(chapterId: string, sceneId: string) { const [elA, elB] = videoEls() if (elA && elB) engine.videoManager.attach(elA, elB) store.setGameEnded(false) engine.startAtScene(chapterId, sceneId) } function skipScene() { engine.skipCurrentScene() } function setSpeed(rate: number) { engine.videoManager.setPlaybackRate(rate) } function getSpeed(): number { return engine.videoManager.getPlaybackRate() } async function isSceneWatched(sceneId: string): Promise { return await saveSystem.isWatched(sceneId) } 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) } } 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 { 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, startAtScene, skipScene, setSpeed, getSpeed, isSceneWatched, saveGame, loadGameFromSlot, refreshSaves, engine, saveSystem, } }