feat: chapter select system, multi-chapter support, scene manager refactor, and docs update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { SceneNode, Choice, EngineEvent, Hotspot } from '../types'
|
||||
import type { SceneNode, Choice, EngineEvent, Hotspot, ChapterInfo } from '../types'
|
||||
import { SceneManager } from './SceneManager'
|
||||
import { VideoManager } from './VideoManager'
|
||||
import { StateManager } from './StateManager'
|
||||
@@ -24,6 +24,11 @@ export class Engine {
|
||||
private qteResolved = false
|
||||
private justCameFromImage = false
|
||||
private loopActive = false
|
||||
private onUnlockChapter: ((chapterId: string) => void) | null = null
|
||||
|
||||
setChapterUnlockHandler(handler: (chapterId: string) => void) {
|
||||
this.onUnlockChapter = handler
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.sceneManager = new SceneManager()
|
||||
@@ -62,6 +67,12 @@ export class Engine {
|
||||
this.qteResolved = false
|
||||
this.loopActive = false
|
||||
|
||||
const chapter = this.sceneManager.getChapterBySceneId(scene.id)
|
||||
if (chapter) {
|
||||
this.onUnlockChapter?.(chapter.id)
|
||||
this.emit('chapterUnlock', chapter)
|
||||
}
|
||||
|
||||
if (scene.onEnter) {
|
||||
this.stateManager.apply(scene.onEnter)
|
||||
}
|
||||
@@ -333,6 +344,29 @@ export class Engine {
|
||||
this.emit('gameEnd')
|
||||
}
|
||||
|
||||
startChapter(chapterId: string) {
|
||||
const chapter = this.sceneManager.getChapter(chapterId)
|
||||
if (!chapter) return
|
||||
|
||||
const scene = this.sceneManager.getScene(chapter.startScene)
|
||||
if (!scene) return
|
||||
|
||||
const defaultVars = chapter.defaultVariables
|
||||
if (defaultVars) {
|
||||
this.stateManager.variables = { ...defaultVars }
|
||||
} else {
|
||||
this.stateManager.init(this.sceneManager.chapters.length > 0
|
||||
? {} // from chapters, use the chapter's defaultVariables or empty
|
||||
: {})
|
||||
}
|
||||
this.stateManager.flags = new Set()
|
||||
this.stateManager.history = []
|
||||
|
||||
this.ended = false
|
||||
this.isInitialScene = false
|
||||
this.goToScene(scene)
|
||||
}
|
||||
|
||||
resumeScene(sceneId: string, savedState: { variables: Record<string, number>; flags: string[]; history: any[] }) {
|
||||
this.stateManager.variables = { ...savedState.variables }
|
||||
this.stateManager.flags = new Set(savedState.flags)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { GameData, SceneNode, Choice, Condition } from '../types'
|
||||
import type { GameData, SceneNode, ChapterInfo, Choice, Condition } from '../types'
|
||||
|
||||
export class SceneManager {
|
||||
private scenes: Record<string, SceneNode> = {}
|
||||
private startScene: string = ''
|
||||
chapters: ChapterInfo[] = []
|
||||
|
||||
load(data: GameData) {
|
||||
this.scenes = data.scenes
|
||||
this.startScene = data.startScene
|
||||
this.chapters = data.chapters || []
|
||||
}
|
||||
|
||||
getScene(id: string): SceneNode | undefined {
|
||||
@@ -23,6 +25,14 @@ export class SceneManager {
|
||||
return Object.keys(this.scenes)
|
||||
}
|
||||
|
||||
getChapterBySceneId(sceneId: string): ChapterInfo | undefined {
|
||||
return this.chapters.find((ch) => ch.startScene === sceneId)
|
||||
}
|
||||
|
||||
getChapter(chapterId: string): ChapterInfo | undefined {
|
||||
return this.chapters.find((ch) => ch.id === chapterId)
|
||||
}
|
||||
|
||||
getCandidateTargetIds(scene: SceneNode, evaluateCondition: (conds?: Condition[]) => boolean): string[] {
|
||||
const targets: string[] = []
|
||||
|
||||
|
||||
@@ -12,13 +12,19 @@ interface SaveRecord {
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
interface UnlockRecord {
|
||||
chapterId: string
|
||||
}
|
||||
|
||||
class SaveDB extends Dexie {
|
||||
saves!: Table<SaveRecord, number>
|
||||
unlocks!: Table<UnlockRecord, string>
|
||||
|
||||
constructor() {
|
||||
super('MovieGameSaves')
|
||||
this.version(2).stores({
|
||||
this.version(3).stores({
|
||||
saves: '++id, slot',
|
||||
unlocks: 'chapterId',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,4 +79,21 @@ export class SaveSystem {
|
||||
async delete(slot: number): Promise<void> {
|
||||
await db.saves.where('slot').equals(slot).delete()
|
||||
}
|
||||
|
||||
async unlockChapter(chapterId: string) {
|
||||
const exists = await db.unlocks.get(chapterId)
|
||||
if (!exists) {
|
||||
await db.unlocks.put({ chapterId })
|
||||
}
|
||||
}
|
||||
|
||||
async isChapterUnlocked(chapterId: string): Promise<boolean> {
|
||||
const record = await db.unlocks.get(chapterId)
|
||||
return !!record
|
||||
}
|
||||
|
||||
async getUnlockedChapters(): Promise<string[]> {
|
||||
const records = await db.unlocks.toArray()
|
||||
return records.map((r) => r.chapterId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +67,19 @@ export interface QTEDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChapterInfo {
|
||||
id: string
|
||||
label: string
|
||||
startScene: string
|
||||
thumbnail?: string
|
||||
defaultVariables?: Record<string, number>
|
||||
}
|
||||
|
||||
export interface GameData {
|
||||
scenes: Record<string, SceneNode>
|
||||
startScene: string
|
||||
variables: Record<string, number>
|
||||
chapters?: ChapterInfo[]
|
||||
}
|
||||
|
||||
export interface ChoiceRecord {
|
||||
@@ -101,3 +110,4 @@ export type EngineEvent =
|
||||
| 'choiceTimeout'
|
||||
| 'hotspotRequest'
|
||||
| 'hotspotUpdate'
|
||||
| 'chapterUnlock'
|
||||
|
||||
Reference in New Issue
Block a user