feat: P15 ending gallery, chapter recap, visited tracking, save system v6

This commit is contained in:
2026-06-09 17:49:07 +08:00
parent 47d6ce50fe
commit 9297117544
14 changed files with 517 additions and 48 deletions

View File

@@ -28,6 +28,7 @@ export class Engine {
private loopActive = false
private onUnlockChapter: ((chapterId: string) => void) | null = null
private onMarkWatched: ((sceneId: string) => void) | null = null
private onMarkVisited: ((sceneId: string) => void) | null = null
setChapterUnlockHandler(handler: (chapterId: string) => void) {
this.onUnlockChapter = handler
@@ -37,6 +38,10 @@ export class Engine {
this.onMarkWatched = handler
}
setMarkVisitedHandler(handler: (sceneId: string) => void) {
this.onMarkVisited = handler
}
constructor() {
this.sceneManager = new SceneManager()
this.videoManager = new VideoManager()
@@ -74,40 +79,16 @@ export class Engine {
}
private goToScene(scene: SceneNode) {
this.currentScene = scene
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)
}
this.onMarkVisited?.(scene.id)
if (scene.videoMuted) {
this.videoManager.setMuted(true)
} else {
this.videoManager.setMuted(false)
}
const bgmUrl = scene.bgmUrl || null
if (bgmUrl) {
this.audioSystem.play(
bgmUrl,
scene.bgmVolume ?? 0.8,
scene.bgmCrossFade ?? 2.0,
scene.bgmDuckLevel,
scene.bgmDuckFade,
)
} else {
this.audioSystem.stop(scene.bgmCrossFade ?? 2.0)
}
this.enterScene(scene)
}
private enterScene(scene: SceneNode) {
this.currentScene = scene
this.qteTriggered = false
this.qteResolved = false
this.loopActive = false
@@ -405,7 +386,7 @@ export class Engine {
this.ended = false
this.isInitialScene = false
this.enterScene(scene)
this.goToScene(scene)
}
destroy() {

View File

@@ -25,6 +25,10 @@ export class SceneManager {
return Object.keys(this.scenes)
}
getScenes(): Record<string, SceneNode> {
return this.scenes
}
getChapterBySceneId(sceneId: string): ChapterInfo | undefined {
return this.chapters.find((ch) => ch.startScene === sceneId)
}

View File

@@ -25,19 +25,25 @@ interface AchievementRecord {
achievementId: string
}
interface VisitedRecord {
sceneId: string
}
class SaveDB extends Dexie {
saves!: Table<SaveRecord, number>
unlocks!: Table<UnlockRecord, string>
watched!: Table<WatchedRecord, string>
achievements!: Table<AchievementRecord, string>
visited!: Table<VisitedRecord, string>
constructor() {
super('MovieGameSaves')
this.version(5).stores({
this.version(6).stores({
saves: '++id, slot',
unlocks: 'chapterId',
watched: 'sceneId',
achievements: 'achievementId',
visited: 'sceneId',
})
}
}
@@ -138,4 +144,16 @@ export class SaveSystem {
const records = await db.achievements.toArray()
return records.map((r) => r.achievementId)
}
async markVisited(sceneId: string) {
const exists = await db.visited.get(sceneId)
if (!exists) {
await db.visited.put({ sceneId })
}
}
async getVisitedSceneIds(): Promise<string[]> {
const records = await db.visited.toArray()
return records.map((r) => r.sceneId)
}
}

View File

@@ -88,12 +88,20 @@ export interface AchievementDef {
condition: Condition
}
export interface EndingDef {
id: string
label: string
sceneId: string
thumbnail?: string
}
export interface GameData {
scenes: Record<string, SceneNode>
startScene: string
variables: Record<string, number>
chapters?: ChapterInfo[]
achievements?: AchievementDef[]
endings?: EndingDef[]
}
export interface ChoiceRecord {