chore: sync latest changes

This commit is contained in:
2026-06-09 17:21:54 +08:00
parent bca137535b
commit 451c6ea025
12 changed files with 503 additions and 28 deletions

View File

@@ -5,6 +5,7 @@ import { StateManager } from './StateManager'
import { ChoiceSystem } from '../systems/ChoiceSystem'
import { QTESystem } from '../systems/QTESystem'
import { AudioSystem } from '../systems/AudioSystem'
import { AchievementSystem } from '../systems/AchievementSystem'
type EventHandler = (...args: any[]) => void
@@ -15,6 +16,7 @@ export class Engine {
choiceSystem: ChoiceSystem
qteSystem: QTESystem
audioSystem: AudioSystem
achievementSystem: AchievementSystem
private currentScene: SceneNode | null = null
private events: Map<EngineEvent, Set<EventHandler>> = new Map()
@@ -42,6 +44,11 @@ export class Engine {
this.choiceSystem = new ChoiceSystem()
this.qteSystem = new QTESystem()
this.audioSystem = new AudioSystem()
this.achievementSystem = new AchievementSystem()
this.stateManager.onAfterApply = (vars) => {
this.achievementSystem.check(vars)
}
this.videoManager.onTimeUpdate(this.onTimeUpdate)
}

View File

@@ -4,6 +4,7 @@ export class StateManager {
variables: Record<string, number> = {}
flags: Set<string> = new Set()
history: ChoiceRecord[] = []
onAfterApply: ((variables: Record<string, number>) => void) | null = null
init(initialVars: Record<string, number>) {
this.variables = { ...initialVars }
@@ -72,6 +73,7 @@ export class StateManager {
break
}
}
this.onAfterApply?.(this.variables)
}
recordChoice(choice: ChoiceRecord) {

View File

@@ -0,0 +1,81 @@
import type { AchievementDef } from '../types'
type UnlockCallback = (achievement: AchievementDef) => void
export class AchievementSystem {
private definitions: AchievementDef[] = []
private unlockedIds: Set<string> = new Set()
private onUnlock: UnlockCallback | null = null
private toastQueue: string[] = []
private toastActive = false
init(defs: AchievementDef[], alreadyUnlocked: string[]) {
this.definitions = defs
this.unlockedIds = new Set(alreadyUnlocked)
this.toastQueue = []
this.toastActive = false
}
setUnlockCallback(cb: UnlockCallback) {
this.onUnlock = cb
}
check(variables: Record<string, number>) {
for (const def of this.definitions) {
if (this.unlockedIds.has(def.id)) continue
const cond = def.condition
const val = variables[cond.variable] ?? 0
let matched = false
switch (cond.op) {
case '==': matched = val === cond.value; break
case '!=': matched = val !== cond.value; break
case '>': matched = val > (cond.value as number); break
case '<': matched = val < (cond.value as number); break
case '>=': matched = val >= (cond.value as number); break
case '<=': matched = val <= (cond.value as number); break
}
if (matched) {
this.unlockedIds.add(def.id)
this.onUnlock?.(def)
this.enqueueToast(def.id)
}
}
}
private enqueueToast(id: string) {
this.toastQueue.push(id)
if (!this.toastActive) {
this.showNextToast()
}
}
private showNextToast() {
if (this.toastQueue.length === 0) {
this.toastActive = false
return
}
this.toastActive = true
// toast is shown by the UI layer watching toastQueue
// UI calls toastDismissed() when the toast animation finishes
}
getToastQueue(): string[] {
return [...this.toastQueue]
}
toastDismissed(id: string) {
this.toastQueue = this.toastQueue.filter((i) => i !== id)
this.showNextToast()
}
getUnlockedIds(): string[] {
return [...this.unlockedIds]
}
getDefinitions(): AchievementDef[] {
return this.definitions
}
}

View File

@@ -21,17 +21,23 @@ interface WatchedRecord {
timestamp: number
}
interface AchievementRecord {
achievementId: string
}
class SaveDB extends Dexie {
saves!: Table<SaveRecord, number>
unlocks!: Table<UnlockRecord, string>
watched!: Table<WatchedRecord, string>
achievements!: Table<AchievementRecord, string>
constructor() {
super('MovieGameSaves')
this.version(4).stores({
this.version(5).stores({
saves: '++id, slot',
unlocks: 'chapterId',
watched: 'sceneId',
achievements: 'achievementId',
})
}
}
@@ -120,4 +126,16 @@ export class SaveSystem {
const records = await db.watched.toArray()
return records.map((r) => r.sceneId)
}
async unlockAchievement(id: string) {
const exists = await db.achievements.get(id)
if (!exists) {
await db.achievements.put({ achievementId: id })
}
}
async getUnlockedAchievements(): Promise<string[]> {
const records = await db.achievements.toArray()
return records.map((r) => r.achievementId)
}
}

View File

@@ -79,11 +79,21 @@ export interface ChapterInfo {
defaultVariables?: Record<string, number>
}
export interface AchievementDef {
id: string
title: string
description: string
icon?: string
hidden?: boolean
condition: Condition
}
export interface GameData {
scenes: Record<string, SceneNode>
startScene: string
variables: Record<string, number>
chapters?: ChapterInfo[]
achievements?: AchievementDef[]
}
export interface ChoiceRecord {
@@ -115,3 +125,4 @@ export type EngineEvent =
| 'hotspotRequest'
| 'hotspotUpdate'
| 'chapterUnlock'
| 'achievementUnlock'