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

@@ -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)
}
}