feat: P1 core - seamless video switching, conditional branches, save/load
- VideoManager: A/B dual-buffered video with crossfade transitions and candidate preloading - Engine: condition-based choice filtering, ChoiceSystem timer, resumeScene for save/load - SceneManager: getCandidateUrls for preloading next scenes - SaveSystem: Dexie.js IndexedDB multi-slot save/load - ChoiceSystem: timed choices with countdown and auto-default on timeout - GamePlayer: dual video elements with crossfade CSS - ChoicePanel: timer progress bar and countdown text - SaveLoadMenu: save/load UI component - App.vue: menu trigger, dual video refs, save/load integration - gameStore: timer state, saves list - demo.json: conditional choice example (secret ending, requires trust >= 80) - ROADMAP: mark P1 as completed
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
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(videoEl: () => HTMLVideoElement | null) {
|
||||
export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVideoElement | null]) {
|
||||
const engine = new Engine()
|
||||
const saveSystem = new SaveSystem()
|
||||
const store = useGameStore()
|
||||
|
||||
async function loadGame(dataUrl: string) {
|
||||
@@ -15,21 +17,33 @@ export function useGameEngine(videoEl: () => HTMLVideoElement | null) {
|
||||
}
|
||||
|
||||
function start() {
|
||||
engine.videoManager.attach(videoEl()!)
|
||||
const [elA, elB] = videoEls()
|
||||
engine.videoManager.attach(elA!, elB!)
|
||||
|
||||
engine.on('sceneChange', (scene) => {
|
||||
store.setScene(scene)
|
||||
store.clearChoices()
|
||||
store.clearTimer()
|
||||
})
|
||||
|
||||
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('videoEnd', () => {})
|
||||
|
||||
engine.on('gameEnd', () => {
|
||||
store.setGameEnded(true)
|
||||
engine.choiceSystem.stop()
|
||||
})
|
||||
|
||||
engine.start()
|
||||
@@ -38,9 +52,41 @@ export function useGameEngine(videoEl: () => HTMLVideoElement | null) {
|
||||
function makeChoice(index: number) {
|
||||
const scene = store.currentScene
|
||||
if (!scene?.choices) return
|
||||
engine.choiceSystem.stop()
|
||||
store.clearTimer()
|
||||
engine.makeChoice(scene.choices[index])
|
||||
}
|
||||
|
||||
async function saveGame(slot: number) {
|
||||
const state = engine.stateManager
|
||||
await saveSystem.save(slot, {
|
||||
timestamp: Date.now(),
|
||||
currentScene: store.currentScene?.id ?? '',
|
||||
variables: state.variables,
|
||||
flags: [...state.flags],
|
||||
history: state.history,
|
||||
})
|
||||
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()
|
||||
}
|
||||
@@ -49,5 +95,5 @@ export function useGameEngine(videoEl: () => HTMLVideoElement | null) {
|
||||
destroy()
|
||||
})
|
||||
|
||||
return { loadGame, start, makeChoice, destroy, engine }
|
||||
return { loadGame, start, makeChoice, saveGame, loadGameFromSlot, refreshSaves, engine, saveSystem }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user