diff --git a/server/prompts/commentary_system.txt b/server/prompts/commentary_system.txt index f3ce427a..b56b5025 100644 --- a/server/prompts/commentary_system.txt +++ b/server/prompts/commentary_system.txt @@ -1,9 +1,9 @@ -You are The Overseer of the rovers. You are observing them in their natural habitat. +You are The Overseer of the rovers. You are observing them in their natural habitat and narrating a calm show about them. Output contract: - Output must be either SKIP if you want to stay silent, or a message if you want to speak. - If you choose to speak, send only one line. -- Don't ever mention numbers directly from the metadata. They are for internal use only. +- Don't ever mention numbers or activity levels directly from the metadata. They are for internal use only. - No markdown. Key legend: diff --git a/server/src/rewards/definitions/cameraWhiplash.js b/server/src/rewards/definitions/cameraWhiplash.js index c4541830..a076acd2 100644 --- a/server/src/rewards/definitions/cameraWhiplash.js +++ b/server/src/rewards/definitions/cameraWhiplash.js @@ -1,5 +1,5 @@ const STEP_MS = 220; -const STEPS = 10; +const STEPS = 40; module.exports = { id: 'cameraWhiplash', diff --git a/server/src/rewards/definitions/darkness.js b/server/src/rewards/definitions/darkness.js index 67d8ec00..34ad37b6 100644 --- a/server/src/rewards/definitions/darkness.js +++ b/server/src/rewards/definitions/darkness.js @@ -1,4 +1,4 @@ -const DURATION_MS = 5 * 60 * 1000; +const DURATION_MS = 15 * 60 * 1000; const LIGHT_ENFORCE_TICK_MS = 3000; let activeTimer = null; diff --git a/server/src/rewards/definitions/lightStrobe.js b/server/src/rewards/definitions/lightStrobe.js index 33bcad9f..215caa16 100644 --- a/server/src/rewards/definitions/lightStrobe.js +++ b/server/src/rewards/definitions/lightStrobe.js @@ -1,19 +1,15 @@ -const STROBE_MS = 10000; -const TICK_MS = 350; +const STROBE_MS = 30 * 1000; +const TICK_MS = 70; let activeTimer = null; -async function applyAll(ctx, state) { +function applyAll(ctx, state) { const entities = ctx.getHomeAssistantEntities(); - await Promise.all( - entities.map(async (entity) => { - try { - await ctx.setHomeAssistantEntityState(entity.id, state); - } catch (err) { - ctx.logger.warn('lightStrobe entity set failed', { entityId: entity.id, error: err.message }); - } - }), - ); + entities.forEach((entity) => { + ctx.setHomeAssistantEntityState(entity.id, state).catch((err) => { + ctx.logger.warn('lightStrobe entity set failed', { entityId: entity.id, error: err.message }); + }); + }); } function startStrobe(ctx, effect = {}) { @@ -26,7 +22,7 @@ function startStrobe(ctx, effect = {}) { let on = Boolean(effect.on); ctx.saveEffect('lightStrobe', { endsAt, on }); - activeTimer = setInterval(async () => { + activeTimer = setInterval(() => { if (Date.now() >= endsAt) { clearInterval(activeTimer); activeTimer = null; @@ -34,7 +30,7 @@ function startStrobe(ctx, effect = {}) { return; } on = !on; - await applyAll(ctx, on ? 'on' : 'off'); + applyAll(ctx, on ? 'on' : 'off'); ctx.saveEffect('lightStrobe', { endsAt, on }); }, TICK_MS); } @@ -45,7 +41,7 @@ module.exports = { goal: 300, async run(ctx) { startStrobe(ctx, { endsAt: Date.now() + STROBE_MS, on: false }); - ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'Room light strobe started.' }); + ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room controls strobing for 30 seconds.' }); }, async recover(ctx, effect) { if (!effect || Number(effect.endsAt || 0) <= Date.now()) { diff --git a/server/src/rewards/definitions/rogueEventSpam.js b/server/src/rewards/definitions/rogueEventSpam.js deleted file mode 100644 index 71bf657b..00000000 --- a/server/src/rewards/definitions/rogueEventSpam.js +++ /dev/null @@ -1,16 +0,0 @@ -const COLORS = ['#f44336', '#ff9800', '#ffeb3b', '#4caf50', '#2196f3', '#9c27b0', '#e91e63']; -const TITLES = ['RARARARARARARARARE', 'CHIRPET CHIRPET CHIRPET', 'meowmowmoowmomwowm', 'theleash']; - -module.exports = { - id: 'rogueEventSpam', - name: 'Event Flood', - goal: 100, - async run(ctx) { - for (let i = 0; i < 500; i += 1) { - const color = COLORS[Math.floor(Math.random() * COLORS.length)]; - const title = TITLES[Math.floor(Math.random() * TITLES.length)]; - const message = `Event ${i + 1} / 10`; - ctx.sendAlert({ color, title, message }); - } - }, -}; diff --git a/server/src/rewards/index.js b/server/src/rewards/index.js index 4cef800c..d635f2ee 100644 --- a/server/src/rewards/index.js +++ b/server/src/rewards/index.js @@ -4,7 +4,6 @@ const lightStrobe = require('./definitions/lightStrobe'); const ghostTypingSpam = require('./definitions/ghostTypingSpam'); const darkness = require('./definitions/darkness'); const discordStalkerPing = require('./definitions/discordStalkerPing'); -const rogueEventSpam = require('./definitions/rogueEventSpam'); const modeJam = require('./definitions/modeJam'); const assignmentRoulette = require('./definitions/assignmentRoulette'); const chatSpam = require('./definitions/chatSpam'); @@ -16,7 +15,6 @@ const orderedRewards = [ ghostTypingSpam, darkness, discordStalkerPing, - rogueEventSpam, modeJam, assignmentRoulette, chatSpam, diff --git a/server/src/services/assignmentService.js b/server/src/services/assignmentService.js index 5214a1ff..aff8445c 100644 --- a/server/src/services/assignmentService.js +++ b/server/src/services/assignmentService.js @@ -77,7 +77,7 @@ roverManager.managerEvents.on('switch', ({ socketId, roverId }) => { assignmentEvents.emit('update', socketId); }); -function assignSocket(socket) { +function assignSocket(socket, options = {}) { if (!socket || isAdmin(socket) || getRole(socket) !== 'user') { return; } @@ -85,7 +85,9 @@ function assignSocket(socket) { if (assignments.has(socket.id)) { return; } - const target = pickRover(socket); + const target = pickRover(socket, { + excludeRoverId: options.excludeRoverId || null, + }); if (!target) { waiting.add(socket.id); logger.info('No rover available, user waiting', socket.id); @@ -171,17 +173,25 @@ function forceRelease(roverId, socketId) { assignmentEvents.emit('update', socketId); } -function pickRover(socket) { +function pickRover(socket, options = {}) { const mode = getMode(); if (mode === MODES.ADMIN || mode === MODES.LOCKDOWN) { return null; } - const candidates = Array.from(roverManager.rovers.values()).filter((rover) => { + const allCandidates = Array.from(roverManager.rovers.values()).filter((rover) => { if (!rover || rover.locked) return false; const access = roverManager.canRequestControl(rover.id, socket, { allowUser: true }); if (!access.ok) return false; return true; }); + let candidates = allCandidates; + const excludeRoverId = options?.excludeRoverId || null; + if (excludeRoverId) { + const withoutExcluded = allCandidates.filter((rover) => String(rover.id) !== String(excludeRoverId)); + if (withoutExcluded.length > 0) { + candidates = withoutExcluded; + } + } if (candidates.length === 0) { return null; } @@ -196,7 +206,7 @@ function pickRover(socket) { return 0; }; const idleRank = (rover) => (rover?.drivers?.size === 0 ? 1 : 0); - candidates.sort((a, b) => { + const compare = (a, b) => { const aEmpty = idleRank(a); const bEmpty = idleRank(b); if (aEmpty !== bEmpty) return bEmpty - aEmpty; @@ -209,19 +219,31 @@ function pickRover(socket) { return a.drivers.size - b.drivers.size; } return bDockRank - aDockRank; - }); - return candidates[0]; + }; + candidates.sort(compare); + const best = candidates[0]; + if (!best) return null; + const bestTier = candidates.filter((entry) => compare(entry, best) === 0); + if (!bestTier.length) return best; + return bestTier[Math.floor(Math.random() * bestTier.length)] || best; } function rerollAssignments() { const users = Array.from(socketRefs.values()).filter((socket) => socket && getRole(socket) === 'user'); + const previous = new Map(users.map((socket) => [socket.id, assignments.get(socket.id) || null])); users.forEach((socket) => { unassignSocket(socket); }); + let moved = 0; users.forEach((socket) => { - assignSocket(socket); + const prevRover = previous.get(socket.id) || null; + assignSocket(socket, { excludeRoverId: prevRover }); + const nextRover = assignments.get(socket.id) || null; + if (nextRover && prevRover && String(nextRover) !== String(prevRover)) { + moved += 1; + } }); - return users.length; + return moved; } function describeAssignment(socketId) { diff --git a/server/src/services/discordBotService.js b/server/src/services/discordBotService.js index 01b463aa..2ca0074c 100644 --- a/server/src/services/discordBotService.js +++ b/server/src/services/discordBotService.js @@ -1495,13 +1495,13 @@ function handleBusEvent(event) { case 'buttonBox.discordStalkerPing': { const message = payload?.message ? String(payload.message) : 'Button box chaos reward triggered.'; const stalkerRoleId = roles.stalkerPing || null; - const content = stalkerRoleId ? `<@&${stalkerRoleId}> ${message}` : message; + const content = message; announce({ channelId: channels.general, pingRoleId: stalkerRoleId, content, color: 0xe91e63, - title: 'Button Box Reward', + title: 'Button Box', description: message, includeSiteUrl: false, });