add albeees style random bullet shooting

This commit is contained in:
2025-11-21 17:03:17 +01:00
parent 34fa80e292
commit 998e67349c
7 changed files with 173 additions and 10 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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`),

View File

@@ -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';

Binary file not shown.

View File

@@ -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 };
};

View File

@@ -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;