129 lines
3.1 KiB
JavaScript
129 lines
3.1 KiB
JavaScript
// Achievements engine — subscribes to bus events, unlocks achievements.
|
|
// On unlock: pushes id into State, emits 'achievement:unlocked' for the widget.
|
|
|
|
import Bus from '../core/bus.js';
|
|
import State from '../core/state.js';
|
|
import catalog from './catalog.js';
|
|
|
|
// In-memory counters per achievement id
|
|
const counters = {};
|
|
// For uniqueField tracking: Set of seen values per achievement id
|
|
const uniqueSets = {};
|
|
// For speedrun: map of windowId -> open timestamp
|
|
const openTimestamps = {};
|
|
|
|
let initialized = false;
|
|
|
|
function hasUnlocked(id) {
|
|
const unlocked = State.get('achievements');
|
|
return unlocked && unlocked.includes(id);
|
|
}
|
|
|
|
function unlock(ach) {
|
|
if (hasUnlocked(ach.id)) return;
|
|
State.addAchievement(ach.id);
|
|
Bus.emit('achievement:unlocked', {
|
|
id: ach.id,
|
|
title: ach.title,
|
|
desc: ach.desc,
|
|
game: ach.game
|
|
});
|
|
}
|
|
|
|
function handleEvent(ach, data) {
|
|
const trigger = ach.trigger;
|
|
|
|
// Event+id specific trigger (easter egg matches)
|
|
if (trigger.id) {
|
|
if (data && data.id === trigger.id) {
|
|
unlock(ach);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Simple event trigger (no count, no accumulate) — unlock on first fire
|
|
if (!trigger.count && !trigger.accumulate && !trigger.uniqueField) {
|
|
unlock(ach);
|
|
return;
|
|
}
|
|
|
|
// Count-based trigger
|
|
if (trigger.count) {
|
|
counters[ach.id] = (counters[ach.id] || 0) + 1;
|
|
if (counters[ach.id] >= trigger.count) {
|
|
unlock(ach);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// UniqueField trigger
|
|
if (trigger.uniqueField) {
|
|
if (!uniqueSets[ach.id]) {
|
|
uniqueSets[ach.id] = new Set();
|
|
}
|
|
const val = data && data[trigger.uniqueField];
|
|
if (val) {
|
|
uniqueSets[ach.id].add(val);
|
|
if (uniqueSets[ach.id].size >= trigger.count) {
|
|
unlock(ach);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Accumulate trigger
|
|
if (trigger.accumulate) {
|
|
const key = trigger.accumulate;
|
|
const value = data && data[key];
|
|
if (typeof value === 'number') {
|
|
counters[ach.id] = (counters[ach.id] || 0) + value;
|
|
if (counters[ach.id] >= trigger.threshold) {
|
|
unlock(ach);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
if (initialized) return;
|
|
initialized = true;
|
|
|
|
// Pre-initialize counters for all achievements
|
|
catalog.forEach(ach => {
|
|
counters[ach.id] = 0;
|
|
uniqueSets[ach.id] = ach.trigger.uniqueField ? new Set() : undefined;
|
|
});
|
|
|
|
// Subscribe to each achievement's trigger event
|
|
catalog.forEach(ach => {
|
|
const trigger = ach.trigger;
|
|
if (trigger.custom === 'speedrun') {
|
|
// Speedrun: listen to app:open and app:close on wm.js
|
|
Bus.on('app:open', (data) => {
|
|
if (data && data.id) {
|
|
openTimestamps[data.id] = Date.now();
|
|
}
|
|
});
|
|
Bus.on('app:close', (data) => {
|
|
if (data && data.id && openTimestamps[data.id]) {
|
|
const delta = Date.now() - openTimestamps[data.id];
|
|
if (delta < 2000) {
|
|
unlock(ach);
|
|
}
|
|
delete openTimestamps[data.id];
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (trigger.event) {
|
|
Bus.on(trigger.event, (data) => {
|
|
handleEvent(ach, data);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
export default { init };
|