Files
MultiRoombaRover/server/src/rewards/definitions/greenMode.js
T
2026-08-08 23:59:47 -04:00

76 lines
2.2 KiB
JavaScript

// Reward Definition: Green Mode
// Purpose: Enables the server-wide green theme and room effect for twenty minutes.
// Scope: Owns button-box timing/recovery while delegating the actual mode to greenModeService.
const DURATION_MS = 20 * 60 * 1000;
let activeTimer = null;
let unsubscribeGreenMode = null;
function clearRuntimeWatchers() {
if (activeTimer) {
clearTimeout(activeTimer);
activeTimer = null;
}
if (unsubscribeGreenMode) {
unsubscribeGreenMode();
unsubscribeGreenMode = null;
}
}
async function stopGreenMode(ctx) {
clearRuntimeWatchers();
await ctx.setGreenMode(false, { source: 'buttonbox:greenModeExpired' });
ctx.clearEffect('greenMode');
}
async function startGreenMode(ctx, effect = {}) {
clearRuntimeWatchers();
const endsAt = Number(effect.endsAt || Date.now() + DURATION_MS);
const remaining = Math.max(0, endsAt - Date.now());
if (remaining <= 0) {
await stopGreenMode(ctx);
return;
}
await ctx.setGreenMode(true, { source: 'buttonbox:greenMode' });
ctx.saveEffect('greenMode', { endsAt });
/*
Access-mode changes disable green mode through greenModeService. Watching
that shared state transition lets the reward discard its persisted effect
immediately, so a restart cannot accidentally revive a reward that was
intentionally ended early.
*/
unsubscribeGreenMode = ctx.onGreenModeChange((enabled) => {
if (enabled) return;
clearRuntimeWatchers();
ctx.clearEffect('greenMode');
});
activeTimer = setTimeout(() => {
stopGreenMode(ctx).catch((err) => {
ctx.logger.warn('green mode reward stop failed', { error: err.message });
});
}, remaining);
}
module.exports = {
id: 'greenMode',
name: 'Green mode',
description: 'Makes the room and server green for 20 minutes.',
goal: 5,
async run(ctx) {
await startGreenMode(ctx, { endsAt: Date.now() + DURATION_MS });
},
async recover(ctx, effect) {
// Recovery must never manufacture a fresh twenty-minute window from a
// missing or corrupt persisted deadline. Treat it as expired and clean up.
if (!Number.isFinite(Number(effect?.endsAt))) {
await stopGreenMode(ctx);
return;
}
await startGreenMode(ctx, effect);
},
};