Initial commit — Meridian OS browser desktop demo
This commit is contained in:
128
js/achievements/engine.js
Normal file
128
js/achievements/engine.js
Normal file
@@ -0,0 +1,128 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user