mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
better buttonboxing
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
module.exports = {
|
||||
id: 'assignmentRoulette',
|
||||
name: 'Rover Reassignment',
|
||||
description: 'Scrambles who is driving which rover.',
|
||||
goal: 300,
|
||||
async run(ctx) {
|
||||
const moved = ctx.rerollAssignments();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Reward Definition: Camera Whiplash
|
||||
// Purpose: Defines the camera-whiplash deterrence reward and timing/strength settings. Scope: Supplies reusable reward metadata and execution parameters for chaos triggers.
|
||||
const STEP_MS = 220;
|
||||
const DURATION_MS = 30 * 1000;
|
||||
const DURATION_MS = 60 * 1000;
|
||||
const STEPS = Math.ceil(DURATION_MS / STEP_MS);
|
||||
|
||||
module.exports = {
|
||||
id: 'cameraWhiplash',
|
||||
name: 'Camera Wiggle',
|
||||
description: 'Makes the rover cameras wiggle around.',
|
||||
goal: 100,
|
||||
async run(ctx) {
|
||||
const rovers = ctx
|
||||
|
||||
@@ -22,6 +22,7 @@ function sleep(ms) {
|
||||
module.exports = {
|
||||
id: 'chatSpam',
|
||||
name: 'Chat Spam',
|
||||
description: 'Floods chat with nonsense messages.',
|
||||
goal: 320,
|
||||
async run(ctx) {
|
||||
const nickname = randLetter();
|
||||
|
||||
@@ -116,6 +116,7 @@ async function startDarkness(ctx, effect) {
|
||||
module.exports = {
|
||||
id: 'darkness',
|
||||
name: 'Darkness',
|
||||
description: 'Disables room lights and headlights for 15 minutes.',
|
||||
isHeadlightBlocked,
|
||||
goal: 400,
|
||||
async run(ctx) {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
module.exports = {
|
||||
id: 'discordPingEveryone',
|
||||
name: 'PING @EVERYONE',
|
||||
goal: 20000,
|
||||
description: 'Pings @everyone in Discord.',
|
||||
dailyLimited: true,
|
||||
goal: 4000,
|
||||
async run(ctx) {
|
||||
ctx.publishEvent({
|
||||
source: 'buttonBoxReward',
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
module.exports = {
|
||||
id: 'discordStalkerPing',
|
||||
name: 'Discord Ping',
|
||||
goal: 9000,
|
||||
description: 'Pings the stalker role in Discord.',
|
||||
dailyLimited: true,
|
||||
goal: 2000,
|
||||
async run(ctx) {
|
||||
ctx.publishEvent({
|
||||
source: 'buttonBoxReward',
|
||||
|
||||
@@ -5,6 +5,7 @@ const DOCK_COMMAND_BASE64 = Buffer.from([143]).toString('base64');
|
||||
module.exports = {
|
||||
id: 'dockPanic',
|
||||
name: 'All Dock',
|
||||
description: 'Forces all rovers into docking mode.',
|
||||
goal: 180,
|
||||
async run(ctx) {
|
||||
const rovers = ctx.listOnlineRovers();
|
||||
|
||||
@@ -16,6 +16,7 @@ function sleep(ms) {
|
||||
module.exports = {
|
||||
id: 'ghostTypingSpam',
|
||||
name: 'Typing Spam',
|
||||
description: 'Fills chat with fake typing indicators.',
|
||||
goal: 220,
|
||||
async run(ctx) {
|
||||
const active = new Set();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Reward Definition: Light Strobe
|
||||
// Purpose: Defines the light-strobe deterrence reward and activation contract. Scope: Encapsulates reward identity, labels, and effect parameters for runtime dispatch.
|
||||
const STROBE_MS = 30 * 1000;
|
||||
const TICK_MS = 200;
|
||||
const TICK_MS = 500;
|
||||
|
||||
let activeTimer = null;
|
||||
|
||||
@@ -40,7 +40,8 @@ function startStrobe(ctx, effect = {}) {
|
||||
module.exports = {
|
||||
id: 'lightStrobe',
|
||||
name: 'Light Strobe',
|
||||
goal: 300,
|
||||
description: 'Makes the room lights flash rapidly for 30 seconds.',
|
||||
goal: 400,
|
||||
async run(ctx) {
|
||||
startStrobe(ctx, { endsAt: Date.now() + STROBE_MS, on: false });
|
||||
ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room controls strobing for 30 seconds.' });
|
||||
|
||||
@@ -53,7 +53,8 @@ function scheduleRestore(ctx, effect = {}) {
|
||||
module.exports = {
|
||||
id: 'modeJam',
|
||||
name: 'Admin Lock',
|
||||
goal: 900,
|
||||
description: 'Temporarily puts the server in admin mode.',
|
||||
goal: 3000,
|
||||
async run(ctx) {
|
||||
const durationMs =
|
||||
MIN_DURATION_MS + Math.floor(Math.random() * (MAX_DURATION_MS - MIN_DURATION_MS + 1));
|
||||
|
||||
@@ -25,6 +25,137 @@ function createButtonBoxCore(deps) {
|
||||
store,
|
||||
} = deps;
|
||||
|
||||
function getTodayKey(now = Date.now()) {
|
||||
/*
|
||||
The daily cap is intentionally based on the server's local calendar day.
|
||||
This matches the operational reality of the deployed server and avoids
|
||||
storing per-user timezone state for a physical shared button box.
|
||||
*/
|
||||
const date = new Date(now);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function getDailyLimit(button) {
|
||||
/*
|
||||
Ceil lets a reward complete in five daily windows even when its goal is
|
||||
not evenly divisible by five. The minimum of one keeps tiny goals usable.
|
||||
*/
|
||||
if (button?.dailyLimited !== true) {
|
||||
return null;
|
||||
}
|
||||
const goal = Number.isFinite(button?.goal) ? Math.max(1, Math.floor(button.goal)) : 1;
|
||||
return Math.max(1, Math.ceil(goal / 5));
|
||||
}
|
||||
|
||||
function resetDailyBucketIfNeeded(button, now = Date.now()) {
|
||||
const today = getTodayKey(now);
|
||||
if (button.dailyDate !== today) {
|
||||
/*
|
||||
Date rollover is handled lazily on the next press/add-count request.
|
||||
That keeps the persisted state correct without needing a background
|
||||
timer that would sit around on the development machine.
|
||||
*/
|
||||
button.dailyDate = today;
|
||||
button.dailyCount = 0;
|
||||
}
|
||||
if (!Number.isFinite(button.dailyCount) || button.dailyCount < 0) {
|
||||
button.dailyCount = 0;
|
||||
}
|
||||
return today;
|
||||
}
|
||||
|
||||
function buildIncrementDescription({ appliedCount, limited }) {
|
||||
if (limited && appliedCount <= 0) {
|
||||
return 'Daily limit reached';
|
||||
}
|
||||
return 'Progress added';
|
||||
}
|
||||
|
||||
function buildIncrementPayload({ button, requestedCount, appliedCount, limited, now }) {
|
||||
const dailyLimit = getDailyLimit(button);
|
||||
return {
|
||||
buttonId: button.id,
|
||||
count: button.count,
|
||||
goal: button.goal,
|
||||
dailyLimited: button.dailyLimited === true,
|
||||
limited,
|
||||
appliedCount,
|
||||
requestedCount,
|
||||
dailyCount: button.dailyCount,
|
||||
dailyLimit,
|
||||
description: buildIncrementDescription({ appliedCount, limited }),
|
||||
ts: now,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneButtonForResponse(button) {
|
||||
/*
|
||||
HTTP callers and overseer tools receive a direct button response instead
|
||||
of waiting for session sync. Include the derived daily limit there too so
|
||||
every public button shape describes the same daily-cap state.
|
||||
*/
|
||||
return {
|
||||
...store.clone(button),
|
||||
dailyLimit: getDailyLimit(button),
|
||||
};
|
||||
}
|
||||
|
||||
async function applyProgress(buttonId, requestedCount = 1) {
|
||||
const state = store.getState();
|
||||
const button = state.buttons.find((entry) => entry.id === buttonId);
|
||||
if (!button) {
|
||||
throw new Error('Unknown button');
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
resetDailyBucketIfNeeded(button, now);
|
||||
|
||||
const requested = Math.max(1, Math.floor(Number(requestedCount) || 0));
|
||||
const dailyLimited = button.dailyLimited === true;
|
||||
const dailyLimit = getDailyLimit(button);
|
||||
/*
|
||||
Most rewards are intentionally uncapped so button-box chaos can still be
|
||||
built up quickly. The daily bucket only constrains rewards that opt into
|
||||
it, currently the Discord pings that can bother people off-site.
|
||||
*/
|
||||
const dailyRemaining = dailyLimited ? Math.max(0, dailyLimit - button.dailyCount) : requested;
|
||||
const appliedCount = dailyLimited ? Math.min(requested, dailyRemaining) : requested;
|
||||
const limited = dailyLimited && appliedCount < requested;
|
||||
|
||||
if (appliedCount > 0) {
|
||||
button.count += appliedCount;
|
||||
if (dailyLimited) {
|
||||
button.dailyCount += appliedCount;
|
||||
}
|
||||
button.lastIncrementAt = now;
|
||||
}
|
||||
|
||||
store.writeState();
|
||||
|
||||
io.emit('buttonBox:increment', buildIncrementPayload({
|
||||
button,
|
||||
requestedCount: requested,
|
||||
appliedCount,
|
||||
limited,
|
||||
now,
|
||||
}));
|
||||
|
||||
while (button.count >= button.goal) {
|
||||
await runRewardForButton(button);
|
||||
/*
|
||||
Reward assignment resets the daily bucket because the new reward should
|
||||
start clean instead of inheriting the just-completed reward's cap usage.
|
||||
*/
|
||||
store.writeState();
|
||||
}
|
||||
|
||||
publishUpdated();
|
||||
return cloneButtonForResponse(button);
|
||||
}
|
||||
|
||||
function publishUpdated() {
|
||||
publishEvent({
|
||||
source: 'buttonBox',
|
||||
@@ -117,55 +248,12 @@ function createButtonBoxCore(deps) {
|
||||
}
|
||||
|
||||
async function applyPress(buttonId) {
|
||||
const state = store.getState();
|
||||
const button = state.buttons.find((entry) => entry.id === buttonId);
|
||||
if (!button) {
|
||||
throw new Error('Unknown button');
|
||||
}
|
||||
|
||||
button.count += 1;
|
||||
button.lastIncrementAt = Date.now();
|
||||
store.writeState();
|
||||
|
||||
io.emit('buttonBox:increment', {
|
||||
buttonId,
|
||||
count: button.count,
|
||||
ts: button.lastIncrementAt,
|
||||
});
|
||||
|
||||
if (button.count >= button.goal) {
|
||||
await runRewardForButton(button);
|
||||
store.writeState();
|
||||
}
|
||||
|
||||
publishUpdated();
|
||||
return store.clone(button);
|
||||
return applyProgress(buttonId, 1);
|
||||
}
|
||||
|
||||
async function addCount(buttonId, amount = 1) {
|
||||
const state = store.getState();
|
||||
const button = state.buttons.find((entry) => entry.id === buttonId);
|
||||
if (!button) {
|
||||
throw new Error('Unknown button');
|
||||
}
|
||||
const inc = Math.max(1, Math.floor(Number(amount) || 0));
|
||||
button.count += inc;
|
||||
button.lastIncrementAt = Date.now();
|
||||
store.writeState();
|
||||
|
||||
io.emit('buttonBox:increment', {
|
||||
buttonId,
|
||||
count: button.count,
|
||||
ts: button.lastIncrementAt,
|
||||
});
|
||||
|
||||
while (button.count >= button.goal) {
|
||||
await runRewardForButton(button);
|
||||
store.writeState();
|
||||
}
|
||||
|
||||
publishUpdated();
|
||||
return store.clone(button);
|
||||
return applyProgress(buttonId, inc);
|
||||
}
|
||||
|
||||
async function recoverEffects() {
|
||||
|
||||
@@ -22,8 +22,12 @@ function createButtonBoxStore(deps) {
|
||||
count: 0,
|
||||
rewardId: null,
|
||||
rewardName: null,
|
||||
rewardDescription: null,
|
||||
dailyLimited: false,
|
||||
rewardNumber: null,
|
||||
goal: null,
|
||||
dailyDate: null,
|
||||
dailyCount: 0,
|
||||
lastIncrementAt: null,
|
||||
lastRewardAt: null,
|
||||
};
|
||||
@@ -67,9 +71,18 @@ function createButtonBoxStore(deps) {
|
||||
|
||||
button.rewardId = reward.id;
|
||||
button.rewardName = reward.name || null;
|
||||
button.rewardDescription = reward.description || null;
|
||||
button.dailyLimited = reward.dailyLimited === true;
|
||||
button.rewardNumber = reward.number;
|
||||
button.goal = reward.goal;
|
||||
button.count = 0;
|
||||
/*
|
||||
A new reward starts a fresh earning window for the button. Resetting the
|
||||
daily bucket here prevents leftover progress from the previous reward from
|
||||
making the newly assigned reward appear capped before anyone presses it.
|
||||
*/
|
||||
button.dailyDate = null;
|
||||
button.dailyCount = 0;
|
||||
}
|
||||
|
||||
function ensureRewardAssignments() {
|
||||
@@ -80,11 +93,19 @@ function createButtonBoxStore(deps) {
|
||||
if (reward && !seen.has(reward.id)) {
|
||||
seen.add(reward.id);
|
||||
button.rewardName = reward.name || null;
|
||||
button.rewardDescription = reward.description || null;
|
||||
button.dailyLimited = reward.dailyLimited === true;
|
||||
button.rewardNumber = reward.number;
|
||||
button.goal = reward.goal;
|
||||
if (!Number.isFinite(button.count) || button.count < 0) {
|
||||
button.count = 0;
|
||||
}
|
||||
if (typeof button.dailyDate !== 'string' || !button.dailyDate) {
|
||||
button.dailyDate = null;
|
||||
}
|
||||
if (!Number.isFinite(button.dailyCount) || button.dailyCount < 0) {
|
||||
button.dailyCount = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
assignNewReward(button);
|
||||
@@ -104,8 +125,12 @@ function createButtonBoxStore(deps) {
|
||||
count: Number.isFinite(loaded.count) ? Math.max(0, Math.floor(loaded.count)) : 0,
|
||||
rewardId: typeof loaded.rewardId === 'string' ? loaded.rewardId : null,
|
||||
rewardName: typeof loaded.rewardName === 'string' ? loaded.rewardName : null,
|
||||
rewardDescription: typeof loaded.rewardDescription === 'string' ? loaded.rewardDescription : null,
|
||||
dailyLimited: loaded.dailyLimited === true,
|
||||
rewardNumber: Number.isFinite(loaded.rewardNumber) ? Math.floor(loaded.rewardNumber) : null,
|
||||
goal: Number.isFinite(loaded.goal) ? Math.max(1, Math.floor(loaded.goal)) : null,
|
||||
dailyDate: typeof loaded.dailyDate === 'string' && loaded.dailyDate ? loaded.dailyDate : null,
|
||||
dailyCount: Number.isFinite(loaded.dailyCount) ? Math.max(0, Math.floor(loaded.dailyCount)) : 0,
|
||||
lastIncrementAt: Number.isFinite(loaded.lastIncrementAt) ? loaded.lastIncrementAt : null,
|
||||
lastRewardAt: Number.isFinite(loaded.lastRewardAt) ? loaded.lastRewardAt : null,
|
||||
};
|
||||
@@ -153,8 +178,27 @@ function createButtonBoxStore(deps) {
|
||||
return state;
|
||||
}
|
||||
|
||||
function getDailyLimit(button) {
|
||||
/*
|
||||
The persisted file stores only the source values. The public snapshot adds
|
||||
this derived limit so clients can render daily status without duplicating
|
||||
button-box reward math in React components.
|
||||
*/
|
||||
if (button?.dailyLimited !== true) {
|
||||
return null;
|
||||
}
|
||||
const goal = Number.isFinite(button?.goal) ? Math.max(1, Math.floor(button.goal)) : 1;
|
||||
return Math.max(1, Math.ceil(goal / 5));
|
||||
}
|
||||
|
||||
function getStateClone() {
|
||||
return clone({ buttons: getState().buttons });
|
||||
const current = getState();
|
||||
return clone({
|
||||
buttons: current.buttons.map((button) => ({
|
||||
...button,
|
||||
dailyLimit: getDailyLimit(button),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user