mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
feat(commands): play a bonk sound on the bonked user's rover
`rs bonk` now publishes a `fun.bonked` event carrying the rover the target is currently driving, and a new audioForwardService listener plays a sound file on it. Wired as an event rather than a direct call so the command layer stays unaware of ffmpeg, matching how the charging-complete cue is already done. The sound file goes at `server/assets/bonk.wav` and is NOT committed here. Note that `server/assets` is the correct home rather than `server/public`: the webui builds to `../server/public` with `emptyOutDir: true`, so anything stored there is deleted by the next build. Details: - The audio is rate limited per rover on a 20s window, separate from the 4s text cooldown. Playback interrupts whatever that rover is forwarding, including a live microphone, so a group of people cannot chain it against one driver. - No sound plays if the target is not currently driving, is not a real user, or is the caller themselves. The text bonk and the tally still work in all cases. - A missing sound file logs once and skips, so the command works on a server that never installs one. A playback failure is caught and logged rather than surfacing as a failed chat command. - Discord bonks play the sound too; only commands needing the caller's own socket are unavailable from there. Server suite: 138 passing, 0 failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eb0db3508f
commit
dc8267073b
@@ -18,6 +18,14 @@ const {
|
||||
|
||||
const TEXT_COOLDOWN_MS = 4 * 1000;
|
||||
|
||||
/*
|
||||
The bonk sound gets its own, much longer rover-scoped window. Playing it
|
||||
interrupts whatever that rover is forwarding — including a live microphone — so
|
||||
the audio must not be spammable even though the text bonk stays snappy, and a
|
||||
group of people bonking one driver cannot chain it either.
|
||||
*/
|
||||
const BONK_SOUND_ROVER_COOLDOWN_MS = 20 * 1000;
|
||||
|
||||
const SLAP_ITEMS = [
|
||||
'a large trout', 'a rolled-up service manual', 'a dead AA battery', 'a docking station',
|
||||
'a suspiciously warm power brick', 'half a roll of duct tape', 'a decommissioned brush guard',
|
||||
@@ -95,10 +103,42 @@ function uwuify(text) {
|
||||
.replace(/!+/g, ' !!');
|
||||
}
|
||||
|
||||
function createFunTextCommands({ io, getNickname, sanitizeMentions, funStatsService, cooldowns, config }) {
|
||||
function createFunTextCommands({
|
||||
io,
|
||||
getNickname,
|
||||
getActiveDrivers,
|
||||
publishEvent,
|
||||
sanitizeMentions,
|
||||
funStatsService,
|
||||
cooldowns,
|
||||
config,
|
||||
}) {
|
||||
const { prefix: commandPrefix } = getCommandConfig(config);
|
||||
const safe = (text) => (sanitizeMentions ? sanitizeMentions(text) : String(text || ''));
|
||||
|
||||
/*
|
||||
Announces the bonk so audioForwardService can play the sound on the rover the
|
||||
target is driving. Published as an event rather than calling the audio pipeline
|
||||
directly, matching how the charging-complete cue is wired: the command layer
|
||||
stays unaware of ffmpeg, and a server without the sound installed simply has
|
||||
nothing listening that can do anything.
|
||||
*/
|
||||
function announceBonk(targetSocket, targetLabel, actorLabelText) {
|
||||
if (!targetSocket || typeof publishEvent !== 'function') return;
|
||||
|
||||
const drivers = getActiveDrivers?.() || {};
|
||||
const roverId = Object.keys(drivers).find((id) => drivers[id] === targetSocket.id) || null;
|
||||
if (!roverId) return;
|
||||
|
||||
if (cooldowns.consume(`bonk:sound:${roverId}`, BONK_SOUND_ROVER_COOLDOWN_MS) > 0) return;
|
||||
|
||||
publishEvent({
|
||||
source: 'funCommands',
|
||||
type: 'fun.bonked',
|
||||
payload: { roverId, targetLabel, actor: actorLabelText },
|
||||
});
|
||||
}
|
||||
|
||||
function reply(message, content) {
|
||||
return message.reply({ content: safe(content), allowedMentions: PLAIN_MENTIONS });
|
||||
}
|
||||
@@ -126,7 +166,7 @@ function createFunTextCommands({ io, getNickname, sanitizeMentions, funStatsServ
|
||||
the counter names, and the flavour text differ, and keeping them in one place
|
||||
means a fix to self-targeting or tally credit applies to all of them.
|
||||
*/
|
||||
function createInteraction({ action, counterGiven, counterTaken, selfReply, render }) {
|
||||
function createInteraction({ action, counterGiven, counterTaken, selfReply, render, onApplied }) {
|
||||
return async function handleInteraction(message, tokens = []) {
|
||||
const selector = tokens.join(' ').trim();
|
||||
if (!selector) {
|
||||
@@ -148,6 +188,10 @@ function createFunTextCommands({ io, getNickname, sanitizeMentions, funStatsServ
|
||||
? funStatsService.bumpActorStats(resolved.actorKey, { label: resolved.label, [counterTaken]: 1 })
|
||||
: null;
|
||||
|
||||
// Side effects run after the tallies so a failure in an optional extra (the
|
||||
// bonk sound) cannot cost the user their recorded bonk.
|
||||
onApplied?.({ actor: gated.label, resolved });
|
||||
|
||||
return reply(message, render({
|
||||
actor: gated.label,
|
||||
actorKey: gated.actorKey,
|
||||
@@ -163,6 +207,7 @@ function createFunTextCommands({ io, getNickname, sanitizeMentions, funStatsServ
|
||||
counterGiven: 'bonksGiven',
|
||||
counterTaken: 'bonksTaken',
|
||||
selfReply: (label) => `${label} bonked themselves. That is between you and the rover.`,
|
||||
onApplied: ({ actor, resolved }) => announceBonk(resolved.socket, resolved.label, actor),
|
||||
render: ({ targetLabel, takenCount }) => {
|
||||
const tally = takenCount ? ` That is their ${ordinal(takenCount)} bonk.` : '';
|
||||
return `🔨 Bonked ${targetLabel}.${tally}`;
|
||||
|
||||
Reference in New Issue
Block a user