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:
2026-06-07 16:48:52 +08:00
parent 42181fe185
commit 937e45c203
16 changed files with 763 additions and 71 deletions

View File

@@ -0,0 +1,72 @@
import Dexie, { type Table } from 'dexie'
import type { SaveData } from '../types'
interface SaveRecord {
id?: number
slot: number
timestamp: number
currentScene: string
variables: string
flags: string
history: string
}
class SaveDB extends Dexie {
saves!: Table<SaveRecord, number>
constructor() {
super('MovieGameSaves')
this.version(1).stores({
saves: '++id, slot',
})
}
}
const db = new SaveDB()
export class SaveSystem {
async save(slot: number, data: Omit<SaveData, 'slot' | 'thumbnail'>): Promise<void> {
const record: SaveRecord = {
slot,
timestamp: Date.now(),
currentScene: data.currentScene,
variables: JSON.stringify(data.variables),
flags: JSON.stringify(data.flags),
history: JSON.stringify(data.history),
}
const existing = await db.saves.where('slot').equals(slot).first()
if (existing) {
await db.saves.update(existing.id!, record)
} else {
await db.saves.add(record)
}
}
async load(slot: number): Promise<SaveData | null> {
const record = await db.saves.where('slot').equals(slot).first()
if (!record) return null
return {
slot: record.slot,
timestamp: record.timestamp,
currentScene: record.currentScene,
variables: JSON.parse(record.variables),
flags: JSON.parse(record.flags),
history: JSON.parse(record.history),
}
}
async listSlots(): Promise<{ slot: number; timestamp: number; sceneLabel: string }[]> {
const records = await db.saves.orderBy('slot').toArray()
return records.map((r) => ({
slot: r.slot,
timestamp: r.timestamp,
sceneLabel: r.currentScene,
}))
}
async delete(slot: number): Promise<void> {
await db.saves.where('slot').equals(slot).delete()
}
}