297 lines
8.0 KiB
TypeScript
297 lines
8.0 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'
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<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,
|
|
startAtScene,
|
|
skipScene,
|
|
setSpeed,
|
|
getSpeed,
|
|
isSceneWatched,
|
|
saveGame,
|
|
loadGameFromSlot,
|
|
refreshSaves,
|
|
engine,
|
|
saveSystem,
|
|
}
|
|
}
|