diff --git a/README.md b/README.md index c9713ff..06c8607 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ NAME|COMMAND|FUNCTION|ALIASES|COST -|-|-|-|- Blaster|`blaster {target}`|Times targeted user out for 60 seconds|`blaster` `blast`|100 Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade`|99 -Silver Bullet|`silverbullet {target}`|Times targeted user out for 30 minutes|`silverbullet` `execute` `{blastin}`|666 +Silver Bullet|`silverbullet {target}`|Times targeted or random vulnerable user out for 30 minutes|`silverbullet` `execute` `{blastin}`|666 TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt`|1000 ## Cheers @@ -214,7 +214,7 @@ NAME|AMOUNT|USAGE|FUNCTION `grenade`|99|`cheer99`|Times a random vulnerable chatter out for 60 seconds. Of failure gives cheerer a grenade `timeout`|100|`cheer100 {target}`|Times specified user out for 1 minute. On failure gives cheerer a blaster `superloot`|150|`cheer150`|Get superloot. Details and drop rates can be found [(here)](#lootbox). -`execute`|666|`cheer666 {target}`|Times specified user out for 30 minutes. On failure gives cheerer a silver bullet +`execute`|666|`cheer666 [target]`|Times specified or random vulnerable user out for 30 minutes. On failure gives cheerer a silver bullet `tnt`|1000|`cheer1000`|Gives 5-10 random vulnerable chatters 60 second timeouts. On failure gives cheerer a TNT ## Point Redeems diff --git a/src/cheers/execute.ts b/src/cheers/execute.ts index 2d72bfc..eda6584 100644 --- a/src/cheers/execute.ts +++ b/src/cheers/execute.ts @@ -7,13 +7,35 @@ import { createTimeoutRecord } from "db/dbTimeouts"; import { parseCheerArgs } from "lib/parseCommandArgs"; import { createCheerEventRecord } from "db/dbCheerEvents"; import { playAlert } from "web/alerts/serverFunctions"; +import { redis } from "lib/redis"; const ITEMNAME = 'silverbullet'; export default new Cheer('execute', 666, async (msg, user) => { const args = parseCheerArgs(msg.messageText); - if (!args[0]) { await handleNoTarget(msg, user, ITEMNAME, false); return; }; - const target = await User.initUsername(args[0].toLowerCase()); + + let target: User | null; + if (!args[0]) { + const vulnsids = await redis.keys('user:*:vulnerable'); + const baseusers = vulnsids.map(a => User.initUserId(a.slice(5, -11))); + const users: User[] = []; + for (const user of baseusers) { + const a = await user; + if (!a) continue; + users.push(a); + }; + if (users.length === 0) { await sendMessage('No vulnerable chatters'); await handleNoTarget(msg, user, ITEMNAME, true); return; }; + target = users[Math.floor(Math.random() * users.length)]!; + await playAlert({ + name: 'blastinRoulette', + user: user.displayName, + targets: users.map(a => a.displayName), + finaltarget: target.displayName + }); + await new Promise((res, _) => setTimeout(res, 6000)); + } else { + target = await User.initUsername(args[0].toLowerCase()); + }; if (!target) { await handleNoTarget(msg, user, ITEMNAME, false); return; }; await getUserRecord(target); diff --git a/src/items/silverbullet.ts b/src/items/silverbullet.ts index 5c13904..468e8bf 100644 --- a/src/items/silverbullet.ts +++ b/src/items/silverbullet.ts @@ -7,6 +7,7 @@ import parseCommandArgs from "lib/parseCommandArgs"; import { timeout } from "lib/timeout"; import { playAlert } from "web/alerts/serverFunctions"; import User from "user"; +import { redis } from "lib/redis"; const ITEMNAME = 'silverbullet'; @@ -20,10 +21,6 @@ export default new Item({ price: 666, execution: async (msg, user, specialargs) => { const messagequery = parseCommandArgs(msg.messageText, specialargs?.activation); - if (!messagequery[0]) { await sendMessage('Please specify a target'); return; }; - const target = await User.initUsername(messagequery[0].toLowerCase()); - if (!target) { await sendMessage(`${messagequery[0]} doesn't exist`); return; }; - await getUserRecord(target); // make sure the user record exist in the database if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; }; await user.setLock(); @@ -31,6 +28,32 @@ export default new Item({ const userObj = await getUserRecord(user); if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any silver bullets!`, msg.messageId); await user.clearLock(); return; }; + let target: User | null; + if (!messagequery[0]) { + const vulnsids = await redis.keys('user:*:vulnerable'); + const baseusers = vulnsids.map(a => User.initUserId(a.slice(5, -11))); + const users: User[] = []; + for (const user of baseusers) { + const a = await user; + if (!a) continue; + users.push(a); + }; + if (users.length === 0) { await user.clearLock(); await sendMessage('No vulnerable chatters', msg.messageId); return; }; + target = users[Math.floor(Math.random() * users.length)]!; + await playAlert({ + name: 'blastinRoulette', + user: user.displayName, + targets: users.map(a => a.displayName), + finaltarget: target.displayName + }); + await new Promise((res, _) => setTimeout(res, 6000)); + } else { + target = await User.initUsername(messagequery[0].toLowerCase()); + }; + if (!target) { await user.clearLock(); await sendMessage(`${messagequery[0]} doesn't exist`); return; }; + + await getUserRecord(target); // make sure the user record exist in the database + const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60 * 30); if (result.status) await Promise.all([ sendMessage(`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`), diff --git a/src/web/alerts/types.ts b/src/web/alerts/types.ts index d9e55d1..c14e2cd 100644 --- a/src/web/alerts/types.ts +++ b/src/web/alerts/types.ts @@ -27,12 +27,18 @@ export type soundAlert = alertBase<'sound'> & { sound: soundAlerts; }; +export type blastinRoulette = alertBase<'blastinRoulette'> & { + targets: string[]; + finaltarget?: string; +}; + export type alert = | userBlastAlert | userExecutionAlert | grenadeExplosionAlert | tntExplosionAlert - | soundAlert; + | soundAlert + | blastinRoulette; type playAlertEvent = { function: 'playAlert'; diff --git a/src/web/alerts/www/public/mariokartbox.ogg b/src/web/alerts/www/public/mariokartbox.ogg new file mode 100644 index 0000000..422d0f7 Binary files /dev/null and b/src/web/alerts/www/public/mariokartbox.ogg differ diff --git a/src/web/alerts/www/src/alerts/blastinRoulette.ts b/src/web/alerts/www/src/alerts/blastinRoulette.ts new file mode 100644 index 0000000..5963ccf --- /dev/null +++ b/src/web/alerts/www/src/alerts/blastinRoulette.ts @@ -0,0 +1,110 @@ +import { blastinRoulette } from "web/alerts/types"; + +function easeOutQuad(t: number) { + return t * (2 - t); +} + +export default async function execute(alert: blastinRoulette) { + const audio = new Audio("/alerts/public/mariokartbox.ogg"); + audio.play(); + const div = document.createElement('div'); + div.classList.add('blastin-roulette'); + + const words = (alert.targets && alert.targets.length) ? alert.targets.slice() : ["..."]; + + // how to center a div + div.style.position = 'fixed'; + div.style.top = '50%'; + div.style.left = '50%'; + div.style.transform = 'translate(-50%, -50%)'; + div.style.display = 'flex'; + div.style.alignItems = 'center'; + div.style.justifyContent = 'center'; + + const text = document.createElement('span'); + text.style.fontFamily = '"Jersey 15"'; + text.style.fontSize = '6rem'; + text.style.fontWeight = '700'; + text.style.lineHeight = '1'; + text.style.textAlign = 'center'; + div.appendChild(text); + + // animation parameters + const totalDuration = 4000; // 4 sec + const finalHold = 2000; // hold final word for 2 sec + const steps = 60; // how many changes before settling + + let lastPick = ''; + let chosenFinal: string | undefined; + for (let i = 0; i < steps; i++) { + const t = i / (steps - 1); + const when = Math.round(easeOutQuad(t) * totalDuration); + setTimeout(() => { + if (i === steps - 1) { + // If a finaltarget was provided, use it as the final word + let pick: string; + if (alert.finaltarget) pick = alert.finaltarget + else { + pick = words[Math.floor(Math.random() * words.length)]; + let attempts = 0; + while (pick === lastPick && attempts < 10 && words.length > 1) { + pick = words[Math.floor(Math.random() * words.length)]; + attempts++; + }; + }; + text.textContent = pick; + lastPick = pick; + chosenFinal = pick; + + // brief flicker effect: toggle opacity a few times + const flickerTimes = 6; + const flickerInterval = 80; // ms + // ensure opacity starts at 1 + text.style.opacity = '1'; + for (let k = 0; k < flickerTimes; k++) { + setTimeout(() => { + text.style.opacity = (k % 2 === 0) ? '0' : '1'; + }, k * flickerInterval); + }; + // ensure fully visible after flicker + setTimeout(() => { text.style.opacity = '1'; }, flickerTimes * flickerInterval); + } else { + // pick a random word different from the previous shown + let pick = words[Math.floor(Math.random() * words.length)]; + let attempts = 0; + // Avoid showing the same word as lastPick + while (pick === lastPick && attempts < 10 && words.length > 1) { + pick = words[Math.floor(Math.random() * words.length)]; + attempts++; + }; + // If the next step is the final and a finaltarget is provided, ensure the penultimate isn't equal to it + if (i === steps - 2 && alert.finaltarget && pick === alert.finaltarget && words.length > 1) { + attempts = 0; + let fallback = words[Math.floor(Math.random() * words.length)]; + while ((fallback === pick || fallback === alert.finaltarget) && attempts < 10 && words.length > 1) { + fallback = words[Math.floor(Math.random() * words.length)]; + attempts++; + }; + pick = fallback; + }; + + text.textContent = pick; + lastPick = pick; + }; + }, when); + }; + + // resolve the completion promise after the final hold + setTimeout(() => { + // chosenFinal should be set by the final step; if not, pick one now + if (!chosenFinal) { + if (alert.finaltarget) chosenFinal = alert.finaltarget as string; + else chosenFinal = words[Math.floor(Math.random() * words.length)]; + } + }, totalDuration + finalHold); + + // total time manager should wait/remove + const duration = totalDuration + finalHold; + + return { duration, alertDiv: div, blocking: true }; +}; diff --git a/src/web/alerts/www/src/alerts/index.ts b/src/web/alerts/www/src/alerts/index.ts index c17e720..fac8cb9 100644 --- a/src/web/alerts/www/src/alerts/index.ts +++ b/src/web/alerts/www/src/alerts/index.ts @@ -4,6 +4,7 @@ import userExecution from "./userExecution"; import grenadeExplosion from "./grenadeExplosion"; import tntExplosion from "./tntExplosion"; import sound from "./sound"; +import blastinRoulette from "./blastinRoulette"; export type AlertRunner = { duration: number; @@ -20,5 +21,6 @@ export default { 'userExecution': userExecution, 'grenadeExplosion': grenadeExplosion, 'tntExplosion': tntExplosion, - 'sound': sound + 'sound': sound, + 'blastinRoulette': blastinRoulette } as AlertMap;