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:
72
engine/systems/SaveSystem.ts
Normal file
72
engine/systems/SaveSystem.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user