import type { AchievementDef } from '../types' type UnlockCallback = (achievement: AchievementDef) => void export class AchievementSystem { private definitions: AchievementDef[] = [] private unlockedIds: Set = 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) { 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 } }