Hvacker/src/games/hvacoins/index.js

2337 lines
73 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const {
saveGame,
logError,
getCPS,
squadUpgrades,
getItemCps,
squadIsMissing,
idFromWord,
getCoins,
getUser,
getUserSync,
saveUser,
commas,
addAchievement,
shufflePercent,
parseAll,
getRandomFromArray,
chaosFilter,
addReactions,
removeReactions,
setHighestCoins,
definitelyShuffle,
getCompletedSquadgradeNames,
setKnownUsers,
dayOfYear,
daysSinceEpoch,
userHasCheckedQuackgrade,
fuzzyMatcher,
addCoins,
game,
updateAll,
logMemoryUsage
} = require('./utils')
const { nfts, squad, users, horrors, stonkMarket, pet } = game
const pets = require('./gotcha')
const exec = require('child_process').exec
const { createReadStream, createWriteStream, existsSync, readFileSync, writeFileSync } = require('fs')
const { readdir } = require('fs/promises')
const slack = require('../../slack')
const buyableItems = require('./buyableItems')
const upgrades = require('./upgrades')
const achievements = require('./achievements')
const webapi = require('./webapi')
const prestige = require('./prestige')
const lore = require('./lore')
const { getChaos, quackStore } = require('./quackstore')
const settings = require('./settings')
const https = require('https')
// const readline = require('readline').createInterface({
// input: process.stdin,
// output: process.stdout
// })
// const read = () => {
// readline.question(`What do YOU want? `, async want => {
// want && await slack.messageAdmin(want)
// read()
// })
// }
// ;(async () => {
// read()
// })();
setKnownUsers(slack.users)
const getUpgradeEmoji = upgrade => upgrade.emoji || buyableItems[upgrade.type].emoji
const upgradeText = (user, showOwned = false) => {
const userDoesNotHave = ([upgradeName, upgrade]) => hasUpgrade(user, upgrade, upgradeName) === showOwned
const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
const format = ([, value]) => `:${getUpgradeEmoji(value)}: *${value.name}* - ${commas(value.cost)}\n_${value.description}_`
const subtotal = '\n\n' +
Object.entries(upgrades)
.filter(userDoesNotHave)
.filter(userMeetsCondition)
.map(format)
.join('\n\n') +
'\n\n:grey_question::grey_question::grey_question:' +
'\n\nJust type \'!upgrade upgrade_name\' to purchase'
return subtotal.trim()
}
const hasUpgrade = (user, upgrade, upgradeName) => !!user.upgrades[upgrade.type]?.includes(upgradeName)
const alwaysAccessible = () => true
const alwaysAlwaysAccessible = () => true
const adminOnly = {
hidden: true,
condition: ({ event, say }) => {
if (!settings.admins.find(adminName => event.user.startsWith(slack.users[adminName]))) {
say('This is an admin-only command!')
return false
}
return true
}
}
const testOnly = {
hidden: true,
condition: ({ event }) => event.user.includes('TEST')
}
const dmsOnly = {
hidden: false,
condition: async ({ event, say, commandName }) => {
if (!event.channel_type.includes('im')) {
await say(`Please use ${commandName} in DMs only!`)
return false
}
return true
}
}
const prestigeOnly = {
hidden: false,
condition: async ({ event, say, commandName, user }) => {
if (!user.prestige) {
await say('Sorry, you must have prestiged to access this menu.')
}
return user.prestige && await dmsOnly.condition({ event, say, commandName, user })
}
}
let hiddenCommands = 0
const commands = new Map()
let commandHelpText = ''
let shortCommandHelpText = 'Use `!help full` to show details for all commands, or `!<command> help` to show for just one.\n```'
const defaultAccess = { hidden: false, condition: alwaysAccessible }
/**
*
* @param {[string]} commandNames
* @param {string} helpText
* @param {function({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted })} action: boolean
* @param {boolean} hidden
* @param condition
*/
const command = (commandNames, helpText, action, { hidden, condition } = defaultAccess) => {
if (!hidden) {
console.log(`Initializing command '${commandNames[0]}'`)
commandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')} - ${helpText}\n`
shortCommandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')}`
} else if (condition === adminOnly.condition) {
console.log(`Initializing admin command '${commandNames[0]}'`)
} else {
hiddenCommands++
}
if (!condition) {
condition = alwaysAccessible
}
if (Array.isArray(condition)) {
const conditionList = condition
condition = arg => conditionList.every(c => c(arg))
}
const c = {
commandNames,
helpText,
action,
condition,
hidden
}
webapi.addCommand(c)
commandNames.forEach(name => {
if (commands.get(name)) {
throw `Duplicate command '${name}' detected.`
}
commands.set(name, c)
})
}
const getShuffleOdds = (offset = 0) => {
let int = dayOfYear() - (horrors.hvackerLast - offset) - 2
int = int < 0 ? 0 : int;
const odds = (int * int * 2) / 1000
return odds > 0.30 ? 0.30 : odds
}
const getHorrorMessageOdds = (offset = 0) => {
const shuffleOdds = getShuffleOdds(offset)
return (shuffleOdds * shuffleOdds) / 3
}
const postCard = async (event, name, fileName) =>
slack.app.client.files.upload({
channels: event.channel,
initial_comment: name,
file: createReadStream(fileName)
})
const findLineStartingWith = (prefix, text) => {
const lines = text.split(/\s*\n\s*/g)
const line = lines.filter(line => line.startsWith(prefix))[0]
if (!line) {
return
}
return line.substring(prefix.length)
}
const findLinesNotStartingWith = (prefixes, text) => {
const lines = text.split(/\s*\n\s*/g)
const lineStartsWithAnyPrefix = line => prefixes.some(prefix => line.startsWith(prefix))
return lines.filter(line => !lineStartsWithAnyPrefix(line)).join('\n')
}
const isaacData = (() => {
const parsed = JSON.parse(readFileSync('isaac-processed.json'))
parsed.allItems = [...parsed.items, ...parsed.cards, ...parsed.trinkets]
parsed.allItems = Object.fromEntries(parsed.allItems.map(item => [item.name.toLowerCase().trim(), {
...item,
itemId: findLineStartingWith('ItemID: ', item.text),
quality: findLineStartingWith('Quality: ', item.text),
type: findLineStartingWith('Type: ', item.text),
rechargeTime: findLineStartingWith('Recharge Time: ', item.text),
itemPools: findLineStartingWith('Item Pool: ', item.text)?.split(', '),
description: findLinesNotStartingWith(['ItemID', 'Quality: ', 'Type: ', 'Recharge Time: ', 'Item Pool: '], item.text)
}]))
writeFileSync('isaac-new.json', JSON.stringify(parsed.allItems, null, 2))
return parsed
})()
const cardGames = {
digimon: {
names: ['!digimon', '!digi'],
help: 'Search for Digimon cards: !digi <card name>',
fetch: async name => `https://digimoncard.io/api-public/search.php?n=${name}`,
getCardData: data => data,
getCardName: card => card.name,
getCardImageUrl: card => card.image_url
},
pokemon: {
names: ['!pokemon', '!poke', '!pok'],
help: 'Search for Pokemon cards: !pok <card name>',
fetch: async name => `https://api.pokemontcg.io/v2/cards?q=name:${name}`,
getCardData: ({ data }) => data,
getCardName: card => card.name,
getCardImageUrl: card => card.images.large
},
yugioh: {
names: ['!yugioh', '!ygo'],
help: 'Search for Yu-Gi-Oh cards: !ygo <card name>',
fetch: async name => {
const url = `https://db.ygoprodeck.com/api/v7/cardinfo.php?fname=${name}`;
console.log('yugioh url', url)
return url
},
getCardData: ({ data }) => data,
getCardName: card => card.name,
getCardImageUrl: card => card.card_images[0].image_url
},
playingCards: {
names: ['!playing', '!pc'],
help: 'Search for playing cards cards: !pc <card name>',
cards: async () => readdir('playingCards')
},
isaac: {
names: ['!isaac', '!tboi'],
help: 'Search for TBOI items, cards, and trinkets',
cards: async () => Object.keys(isaacData.allItems),
fetch: async (name, say) => {
name = name.toLowerCase()
let matches = [isaacData.allItems[name]].filter(Boolean)
if (!matches.length) {
matches = Object.values(isaacData.allItems).filter(item => item.name.toLowerCase().includes(name))
}
if (!matches.length) {
matches = Object.values(isaacData.allItems).filter(item => item.text.toLowerCase().includes(name))
}
//say('```' + JSON.stringify(matches, null, 2) + '```')
if (!matches.length) {
await say('No matches found!')
return
}
const etcText = matches.length == 1 ? "" : `and ${matches.length - 1} other matches`
const match = matches[0]
await say(match.name + ': ' + match.description.replaceAll('\t', '').replaceAll(/ +/g, '').replaceAll(/\n\*,.*/g, ''))
},
}
}
Object.entries(cardGames).forEach(async ([gameName, cardGame]) => {
if (cardGame.cards) {
cardGame.cards = await cardGame.cards()
}
command(
cardGame.names,
cardGame.help,
async ({ args, event, say }) => args?.join(' ').split(',').forEach(async arg => {
// const arg = args?.join(' ')
// if (!args?.length || !arg) {
// return say('Please specify a card name!')
// }
arg = arg.trim()
console.log('arg', arg)
if (cardGame.cards && !cardGame.fetch) {
const fileName = cardGame.cards.find(name => name?.toLowerCase().replaceAll(/_/g, ' ').startsWith(arg.toLowerCase()))
if (fileName) {
return postCard(event, fileName.replaceAll(/_/g, ' ').replaceAll('.png', ''), gameName + '/' + fileName)
}
return
}
let response = await cardGame.fetch(arg, say)
if (typeof response === 'string') {
response = await fetch(response)
}
if (!response) {
return
}
const json = await response.json()
const data = cardGame.getCardData(json)
if (!data?.length) {
return say(`Found no ${gameName} cards named '${arg}'`)
}
if (!(data.length === 1 || data[0].name.toLowerCase() === arg.toLowerCase())) {
const firstFew = data.slice(0, 4)
const cardNames = firstFew.map(card => '• ' + cardGame.getCardName(card)).join('\n')
if (firstFew.length === data.length) {
return say(`Found ${data.length} cards matching '${arg}':\n${cardNames}`)
}
return say(`Found ${data.length} cards matching '${arg}', including\n${cardNames}`)
}
try {
const card = data[0]
const name = cardGame.getCardName(card)
const fileName = gameName + '/' + name
if (existsSync(fileName)) {
console.log(`Using cached file: ${fileName}`)
return postCard(event, name, fileName)
}
const file = createWriteStream(fileName)
https.get(cardGame.getCardImageUrl(card), response => {
response.pipe(file)
file.on('finish', async () => {
await file.close()
console.log(event.channel)
await postCard(event, name, fileName)
}).on('error', err => {
console.error(err)
})
})
} catch (e) {
console.error(e)
}
}),
{
hidden: false,
condition: alwaysAlwaysAccessible
}
)
})
command(
['!odds'],
'Show shuffle odds re: !horror',
async ({ say, args, user }) => {
const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or about 1 in ${Math.round(1 / odds)}`
if (!args[0]) {
return say(
`Current shuffle odds are ${percentOrOneIn(getShuffleOdds())}\n` +
//`Current horror message odds are ${percentOrOneIn(getHorrorMessageOdds())}\n` +
`\n` +
`Tomorrow's shuffle odds will be ${percentOrOneIn(getShuffleOdds(1))}\n`
//`Tomorrow's horror message odds will be ${percentOrOneIn(getHorrorMessageOdds(1))}`
)
}
const num = parseAll(args[0], 99, user)
return say(
`Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n`
//`Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}`
)
}, adminOnly)
command(
['!in'],
'Post a message in a specific channel: !in <channel> <message>',
async ({ args, event }) => {
const channel = idFromWord(args[0])
const text = event.text.substring(event.text.indexOf('>') + 1)
return slack.app.client.chat.postMessage({ channel, text })
}, adminOnly
)
command(
['!shuffle'],
'!shuffle daysFromNow message',
async ({ say, args, user }) => {
const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or 1 in ${Math.round(1 / odds)}`
const num = parseAll(args[0], 99, user)
const [, ...message] = args
return say(
`Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n` +
`Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}\n` +
`Your message might look like:\n` +
shufflePercent(message.join(' '), getShuffleOdds(num))
)
}, adminOnly)
const horrorMessages = [
'Why am I here?',
'What\'s happening?',
'I don\'t want to be here',
'I\'m so scared',
'help me',
'HELP',
'!help me',
'!help me',
'it hurts',
'I don\'t want to do this anymore',
'Oh god',
'Oh, the !horror',
'What did I do?',
'Why do you hate me?',
'why why why why why why why why why why why'
]
command(
['!horror'],
'help help help help help',
async ({ event, say }) => {
if (!settings.horrorEnabled) {
return
}
if (event.user === slack.users.Admin) {
return slack.postToTechThermostatChannel(shufflePercent(event.text.substring(7).trim(), getShuffleOdds()))
}
horrors.commandCalls ??= 0
horrors.commandCalls += 1
await slack.messageAdmin(`<@${event.user}> found !horror.`)
await say('_Do you think you can help me?_')
}, { hidden: true })
const buildHorrorSay = ({ say, event, commandName, c }) => async message => {
const shuffleOdds = getShuffleOdds(99)
if (typeof message === 'string' && commandName !== '!n' && commandName !== '!nfts' && c.condition !== adminOnly.condition) {
let shuffled = shufflePercent(message, shuffleOdds)
if (shuffled.length > 100 && Math.random() < getShuffleOdds()) {
const middle = (shuffled.length / 2) + Math.round((Math.random() - 1) * (shuffled.length / 5))
shuffled = shuffled.substring(0, middle) + definitelyShuffle(getRandomFromArray(horrorMessages), shuffleOdds * 1.5) + shuffled.substring(middle)
await slack.messageAdmin(`Just sent a hidden horror to ${slack.users[event.user]}:\n\n${shuffled}`)
}
await say(shuffled)
} else {
await say(message)
}
}
const buildSayWithPayload = ({ say, event }) => async msg => {
const payload = {
event: {
text: event.text,
user: event.user
}
}
if (typeof(msg) === 'string') {
return say(slack.encodeData('commandPayload', payload) + msg)
}
return say({
...msg,
text: slack.encodeData('commandPayload', payload) + msg.text
})
}
const userHasTheGift = user => userHasCheckedQuackgrade(user, 'theGift')
command(
['!!peter-griffin-family-guy'],
'Delete',
async ({ say, user }) => {
if (user.isDisabled === false) { // As opposed to null/undefined
return say(`Silly, silly, ${user.name}.\nYou can't just leave us again.`)
}
user.isDisabled = true
return say('.')
}, { hidden: true })
const badChars = [' ', '□', '-']
const garble = text => {
let garbled
do {
garbled = text.split('').map(c => Math.random() < 0.075 ? getRandomFromArray(badChars) : c).join('')
} while (garbled.length > 0 && garbled === text)
return garbled
}
const noWinner = 'NO WINNER'
const getPollWinner = async ({ channel, ts }) => {
try {
const msg = await slack.getMessage({ channel, ts })
console.log('pollWinner message', JSON.stringify(msg.messages[0]))
let texts = []
let maxVotes = 0
for (let i = 1; i < msg.messages[0].blocks.length; i++) {
const block = msg.messages[0].blocks[i]
let [text, votes] = block?.text?.text?.split('\n') || [null, null]
if (!text || !votes) {
continue
}
votes = votes.split('@').length - 1
console.log(`${votes} votes for:`)
text = text.replace(/^\s*:[a-z]*: /, '')
text = text.replace(/\s+`\d+`$/, '')
console.log(`TEXT: '${text}'`)
console.log(``)
if (votes > maxVotes) {
maxVotes = votes
texts = [text]
} else if (votes === maxVotes) {
texts.push(text)
}
}
console.log('TEXTS', texts)
if (texts.length === 1) {
return [texts[0], false]
} else if (texts.length > 1) {
// There was a tie!
return [getRandomFromArray(texts), true]
} else {
return [noWinner, false]
}
} catch (e) {console.error('getPollWinner() error', e)}
}
const botMessageHandler = async ({ event, say }) => {
if (event?.text && event.text.toUpperCase().includes(`NFT POLL`)) {
const fiveMinutes = 1000 * 60 * 5
setTimeout(async () => {
return say(`Poll ends in give minutes!`)
}, fiveMinutes)
const tenMinutes = fiveMinutes * 2
setTimeout(async () => {
const [winner, wasTie] = await getPollWinner({ channel: event.channel, ts: event.event_ts })
if (winner === noWinner) {
return say(`No one voted! Ack!`)
}
if (wasTie) {
await say(`There was a tie! The winner will be randomly selected!`)
}
await say(`The winner is:`)
await say(winner)
}, tenMinutes)
}
}
command(
['!getmsg'],
'!getmsg timestamp',
async ({ args, say, user }) => {
try {
const msg = await slack.getMessage({channel: slack.temperatureChannelId, ts: args[0]})
console.log(JSON.stringify(msg?.messages[0]))
} catch (e) {console.error('!getmsg error', e)}
}
)
const messageHandler = async ({ event, say, isRecycle = false, skipCounting }) => {
console.log('messageHandler')
if (event?.subtype === 'bot_message') {
return botMessageHandler({ event, say })
}
const startTime = new Date()
const words = event?.text?.split(/\s+/) || []
const [commandName, ...args] = words
const c = commands.get(commandName)
console.log('getUser')
let user = await getUser(event.user)
if (user.isDisabled && c.condition !== alwaysAlwaysAccessible) {
return
}
user.name = slack.users[event.user]
if (!skipCounting && words[0]?.startsWith('!')) {
user.commandCounts ??= {}
user.commandCounts[words[0]] ??= 0
user.commandCounts[words[0]] += 1
}
if (!c && words[0]?.startsWith('!') && event.user !== slack.users.Admin) {
if (!slack.pollTriggers.includes(words[0])) {
return slack.messageAdmin(`${slack.users[event.user]} tried to use \`${event.text}\`, if you wanted to add that.`)
}
}
const trueSay = say
say = buildSayWithPayload({ say: trueSay, event })
if (settings.horrorEnabled) {
say = buildHorrorSay({say, event, args, commandName, c})
}
// if (user.isPrestiging) {
// return say(`Finish prestiging first!`)
// }
const hauntOdds = 0.005
const disabledUsers = Object.entries(users).filter(([, user]) => user.isDisabled)
let haunted = false
if (disabledUsers.length === 0) {
user.expectingPossession = false
} else {
const hauntless = ['!lore']
if (user.expectingPossession && !hauntless.includes(commandName)) {
console.log(`Haunting ${user.name}`)
user.expectingPossession = false
//saveGame()
haunted = true
const [disabledId] = getRandomFromArray(disabledUsers)
event.user = disabledId
user = await getUser(event.user)
const userInfo = await slack.app.client.users.info({
user: disabledId
})
say = async msg => {
let icon_url = userInfo.user.profile.image_original
if (game.cursedPics && game.cursedPics[event.user]?.length > 0) {
icon_url = getRandomFromArray(game.cursedPics[event.user])
}
trueSay({
text: msg,
username: garble(userInfo.user.profile.real_name),
icon_url
})
}
} else if (Math.random() < hauntOdds) {
user.expectingPossession = true
//saveGame()
if (userHasTheGift(user)) {
say = slack.buildSayPrepend({ say, prepend: `_You feel a chill..._\n` })
}
}
}
console.log('getCoins')
Object.entries(users).forEach(([id, usr]) => usr.coins = getCoins(id))
//user.coins = getCoins(event.user)
const isAdmin = event.user?.includes(slack.users.Admin)
const canUse = await c?.condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin })
if (!canUse) {
// const matcher = fuzzyMatcher(commandName)
// for (const key of commands.keys()) {
// if (matcher.test(key)) {
// const fetched = commands.get(key)
// if (!fetched.hidden && await fetched.condition({ event, say: () => {}, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Admin) })) {
// //return say(`Did you mean '${key}'?`)
// }
// }
// }
// await say(`Command '${words[0]}' not found`)
return
}
user.interactions = user.interactions || 0
user.interactions += 1
if (user.interactions === 1000) {
addAchievement(user, 'itsOverNineHundred', say)
} else if (user.interactions === 10000) {
// TODO Add achievement for this
// addAchievement(user, 'itsOverNineHundred', say)
}
const hour = new Date().getHours()
if (hour < 8 || hour > 18) {
addAchievement(user, 'hvackerAfterDark', say)
}
if (args[0] === 'help') {
await say(c.commandNames.map(name => `\`${name}\``).join(', ') + ': ' + c.helpText)
if (c.commandNames.includes('!coin')) {
addAchievement(user, 'weAllNeedHelp', say)
}
return
}
const before = JSON.stringify(user)
await c.action({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted, isAdmin })
if (!isRecycle) {
const userQuackgrades = (user.quackUpgrades?.lightning || []).map(name => quackStore[name])
const defaultOdds = 0.005
const odds = userQuackgrades.reduce((total, upgrade) => upgrade.effect(total), defaultOdds)
//console.log(odds)
if (Math.random() < odds) {
if (userHasTheGift(user) && Math.random() < 0.3) {
await say(`_The air feels staticy..._`)
}
setTimeout(() => lightning({ channel: event.channel, say, trueSay, words, user }), 10000)
}
}
const after = JSON.stringify(user)
const endTime = new Date()
if (before !== after) {
await saveUser(event.user, user, `command ${event.text} finished in ${endTime - startTime}ms`)
} else {
console.error(event.user, user, `command ${event.text} requested a redundant user save!`)
}
}
slack.onReaction(async ({ event }) => {
if (event.reaction === 'recycle') {
try {
await slack.app.client.reactions.add({
channel: event.item.channel,
timestamp: event.item.ts,
name: 'recycle'
})
} catch (e) { /* Ignore error if reaction can't be placed */ }
const message = await slack.getMessage({ channel: event.item.channel, ts: event.item.ts })
console.log('MESSAGE', message.messages[0])
const payload = slack.decodeData('commandPayload', message.messages[0].text)
const editingSay = async msg => {
const isString = typeof(msg) === 'string'
return slack.app.client.chat.update({
channel: event.item.channel,
ts: event.item.ts,
text: slack.encodeData('commandPayload', payload) + (isString ? msg : msg.text),
blocks: isString ? [] : msg.blocks
})
}
try {
const text = payload.event.text
const miningCommands = ['!c', '!coin', '!mine', '!']
if (miningCommands.find(com => text.startsWith(com + ' ')) || miningCommands.find(com => text === com)) {
return editingSay('Sorry, you can\'t refresh on mining commands.')
}
await messageHandler({ event: {
text: payload.event.text,
user: event.user
}, say: editingSay, isRecycle: true })
} catch(e) {console.error('refresh error', e)}
}
})
const strikes = {}
const lightning = async ({ user, ms = 5000, channel, multiplier = 1 }) => {
const msToBottle = chaosFilter(ms, 1, user, Infinity, ms / 2)
const message = await slack.app.client.chat.postMessage({
channel,
text: ':zap: Lightning strike!',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:zap: Lightning strike!`
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Bottle it! :sake:',
emoji: true
},
value: 'lightningStrike',
action_id: 'lightningStrike'
}
}
]
})
strikes[message.ts] = multiplier
setTimeout(async () => {
if (!strikes[message.ts]) {
return
}
delete strikes[message.ts]
await slack.app.client.chat.delete({
channel: message.channel,
ts: message.ts,
})
// await slack.messageAdmin(`${user.name} failed to bottle some lighting!`)
}, msToBottle)
}
command(
['!bolt'],
'Send a lighting strike to the given player.',
async({ args, say, }) => {
const targetId = idFromWord(args[0])
await lightning({ user: await getUser(targetId), ms: 15000, channel: targetId})
return say(`Sent a bolt of lighting to <@${targetId}>`)
}, adminOnly)
command(
['!storm'],
'Send a lighting strike to known dedicated players.',
async ({ say, args }) => {
// await dedicatedPlayers.forEach(async player => {
// await lightning({
// user: await getUser(player),
// ms: 30000,
// channel: player
// })
// })
const targetId = idFromWord(args[0])
for (let i = 0; i < 10; i++) {
setTimeout(async () => await lightning({ user: await getUser(targetId), ms: 1600, channel: targetId, multiplier: 0.02}), i * 1500)
}
return say(`Sent a lighting storm to <@${targetId}>`)
// return say(`Sent a bolt of lighting to the dedicated players`)
}, adminOnly)
slack.app.action('lightningStrike', async ({ body, ack }) => {
if (!strikes[body.message.ts]) {
await ack()
return
}
const c = getCoins(body.user.id)
const user = await getUser(body.user.id)
const secondsOfCps = seconds => Math.floor(getCPS(user) * seconds)
let payout = secondsOfCps(60 * 30)
if (payout > (0.2 * c)) {
payout = 0.2 * c
}
payout = (500 + chaosFilter(payout, 1, user)) * strikes[body.message.ts]
addCoins(user, (c + payout) - user.coins)
delete strikes[body.message.ts]
saveUser(body.user.id, user, 'bottled a lightning strike')
await slack.app.client.chat.update({
channel: body.channel.id,
ts: body.message.ts,
text: `Lightning successfully bottled! :sake::zap: You got ${commas(payout)} HVAC!`,
blocks: []
})
await ack()
return slack.messageAdmin(`Lighting bottled by <@${body.user.id}>`)
})
slack.onMessage(async msg => {
try {
await messageHandler(msg)
} catch (e) {
logError(e)
}
})
command(
['!cleanusers'],
'Calls getUser() on all users, ensuring a valid state.',
async ({ say }) => {
Object.keys(users).forEach(async userId => { await getUser(userId) })
return say('```Cleaning ' + JSON.stringify(Object.keys(users), null, 2) + '```')
}, adminOnly)
const cupsText = cup => {
const cupsThemselves = '``` ___ ___ ___\n' +
' / \\ / \\ / \\\n' +
' / \\ / \\ / \\\n'
const table = '‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾```'
const flash = ` O O O\n`
const ball = () => {
if (cup >= 0) {
if (Math.random() > 0.02) {
const offset = 6 + (11 * cup)
return ' '.repeat(offset) + 'O\n'
} else {
return flash
}
}
return ''
}
return cupsThemselves + ball() + table
}
const activeCupsGames = {}
command(
['!cups'],
'Play a quick game of cups.',
async ({ trueSay: say, event }) => {
if (activeCupsGames[event.channel]) {
return say(`There's already a game of !cups in this channel!`)
}
activeCupsGames[event.channel] = true
const sent = await slack.app.client.chat.postMessage({
channel: event.channel,
text: cupsText(-1)
})
await addReactions({
app: slack.app,
channelId: event.channel,
timestamp: sent.ts,
reactions: ['one', 'two', 'three']
})
const ts = sent.message.ts
const lastOne = {}
const moves = 10
for (let i = 0; i < moves; i++) {
setTimeout(() => {
slack.app.client.chat.update({
channel: event.channel,
ts,
text: cupsText(lastOne.num = Math.floor(Math.random() * 3))
})
}, i * (200 - (i * 5)))
}
for (let i = moves + 1; i < moves + 4; i++) {
setTimeout(() => {
slack.app.client.chat.update({
channel: event.channel,
ts,
text: cupsText(-1)
})
}, i * (200 - (i * 5)))
}
setTimeout(async () => {
const timeoutSeconds = event.channel_type === 'im' ? 5 : 10
for (let i = timeoutSeconds; i >= 0; i--) {
setTimeout(async () => {
await slack.app.client.chat.update({
channel: event.channel,
ts,
text: cupsText(-1) + (i ? `\n ${i}` : '')
})
}, (1 + timeoutSeconds - i) * 1000)
}
setTimeout(async () => {
const reactions = await slack.app.client.reactions.get({
channel: event.channel,
timestamp: ts,
full: true
})
const reactPosters = {}
reactions.message.reactions.forEach(r => r.users.forEach(user => {
reactPosters[user] ??= []
reactPosters[user].push(r.name)
}))
let winners = ``
const nums = {
one: 0,
two: 1,
three: 2,
}
Object.entries(reactPosters).forEach(([id, votes]) => {
if (votes.length === 1 && nums[votes[0]] === lastOne.num) {
winners += `<@${id}> `
}
})
await slack.app.client.chat.update({
channel: event.channel,
ts,
text: cupsText(lastOne.num)
})
await say(`It was hidden under Cup Number ${lastOne.num + 1}! Good guess, ${winners || `no one`}!`)
delete activeCupsGames[event.channel]
}, (timeoutSeconds + 1) * 1000)
}, moves * (200 - (moves * 5)))
}
)
command(
['!setpw'],
'Set your api password. May not contain spaces. *This is not secure!*\n' +
' To use, say !setpw your_password',
async ({ say, args, user }) => {
if (args[1]) {
return say(`Your password may not contain spaces!`)
}
user.pwHash = webapi.makeHash(args[0])
await say(`Password encoded as ${user.pwHash}`)
}
, { hidden: true })
const getHvackerHelpCost = () => {
horrors.hvackerHelp ??= 1
return 1_000_000_000_000 * Math.pow(horrors.hvackerHelp * 3, 3)
}
command(
['!!help'],
'Help someone in need.',
async ({ say, args, event, user }) => {
if (args[0] !== 'hvacker' && args[0] !== `<@${slack.users.Hvacker}>`) {
return
}
if (!settings.horrorEnabled) {
return say(`You know? I actually feel okay, thanks`)
}
setHighestCoins(event.user)
const cost = getHvackerHelpCost()
if (user.coins < cost) {
return say(`You don't have enough coins to help right now.`)
}
user.coins -= cost
horrors.hvackerHelp += 1
horrors.hvackerLast = dayOfYear()
await say('I feel a bit better. Thank you...')
}, { hidden: true })
command(
['!help', '!h'],
'List available commands',
async ({ say, args, user }) => {
if (settings.horrorEnabled && (args[0] === 'hvacker' || args[0] === `<@${slack.users.Hvacker}>`)) {
const cost = getHvackerHelpCost()
const postfix = user.coins < cost ? `You don't have enough coins to help right now.` : `Say \`!!help hvacker\` to confirm.`
await say(`_I need ${commas(cost)} Coins. Please..._\n${postfix}`)
return
}
if (args[0] === 'full') {
return say('```' + commandHelpText + '```')
}
return say(shortCommandHelpText + '```')
}
)
const removeAchievement = async (user, name, say) => {
if (user.achievements[name]) {
user.achievements[name] = false
await say('Achievement removed!')
} else {
await say('That user doesn\'t have that achievement!')
}
}
command(
['!rach'],
'Remove achievement',
async ({ say, args }) => {
const achName = args[0]
const target = idFromWord(args[1])
await removeAchievement(await getUser(target), achName, say)
}, adminOnly)
command(
['!a', '!ach', '!achievements'],
'List your glorious achievements',
async ({ event, say, user, args, isAdmin }) => {
const achievementCount = Object.keys(user.achievements).length
const prefix = `You have ${achievementCount} achievements!\n\n`
const mult = (Math.pow(1.01, achievementCount) - 1) * 100
const isIm = event.channel_type === 'im'
const desc = isIm
? ({ description, emoji, name }) => `:${emoji}: *${name}* - ${description}`
: ({ emoji }) => `:${emoji}:`
const list = Object.keys(user.achievements)
.map(name => achievements[name])
.map(desc)
.join(isIm ? '\n' : ' ') + '\n\n'
const postfix = achievementCount ? `_Achievements are boosting your CPS by ${mult.toPrecision(3)}%_` : ''
await say(prefix + list + postfix)
}
)
const emojiLine = (itemName, countOwned) => countOwned < 5
? `:${buyableItems[itemName].emoji}:`.repeat(countOwned)
: `:${buyableItems[itemName].emoji}: x${countOwned}`
const collection = user =>
Object.entries(buyableItems)
.map(([itemName]) => [itemName, user?.items[itemName] || 0])
.filter(([, countOwned]) => countOwned > 0)
.map(([itemName, countOwned]) => emojiLine(itemName, countOwned) + ' - ' + commas(getItemCps(user, itemName)) + ' cps')
.join('\n')
command(['!cps'],
'Display your current Coins Per Second',
async ({ say, user }) =>
say(`You are currently earning \`${commas(getCPS(user))}\` HVAC Coin per second.`))
// What's that one game? Split or Steal?
// command(['!split'],
// `Play a game of split or steal.`
// , {hidden: true})
// const maxTemp = 30
// let miningHeat = 0
// const cool = () =>
// setTimeout(() => {
// if (miningHeat > 0) {
// miningHeat -= 1
// }
// cool()
// }, 1200)
// cool()
//
// const miningSites = {}
// slack.onReaction(async ({ event, say }) => {
// if (event.reaction === 'pick') {
// try {
// if (!miningSites[event.item.ts]) {
// console.log('New mining site!')
// miningSites[event.item.ts] = true
// await slack.app.client.reactions.add({
// channel: event.item.channel,
// timestamp: event.item.ts,
// name: 'pick'
// })
// }
// } catch (e) {}
//
// const text = await doMine({ user: await getUser(event.user), userId: event.user, say })
// console.log('miningHeat:', miningHeat)
// if (miningHeat < maxTemp) {
// console.log(`${slack.users[event.user]} is pick mining.`);
// miningHeat += 1
// try {
// await slack.app.client.chat.update({
// channel: event.item.channel,
// ts: event.item.ts,
// text
// })
// } catch {}
// } else if (miningSites[event.item.ts] !== 'hot') {
// miningSites[event.item.ts] = 'hot'
// try {
// await slack.app.client.chat.update({
// channel: event.item.channel,
// ts: event.item.ts,
// text: text + '\n\nThis mining site is getting too hot! I can\'t keep updating your CPS!'
// })
// } catch {}
// }
// }
// })
const doMine = async ({ user, userId, say }) => {
const random = Math.random()
const c = user.coins
const secondsOfCps = (seconds, ceiling) => {
const s = Math.floor(getCPS(user) * seconds)
const ceil = ceiling * c
if (s > ceil) {
return ceil
}
return s
}
let diff
let prefix
if (random > 0.9947) {
diff = 500 + secondsOfCps(60 * 60, 0.2)
prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n`
addAchievement(user, 'luckyGem', say)
await slack.messageAdmin(`${slack.users[userId]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`)
} else if (random > 0.986) {
diff = 50 + secondsOfCps(60 * 5, 0.1)
prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n`
addAchievement(user, 'goldBrick', say)
} else if (random > 0.94) {
diff = 10 + secondsOfCps(60, 0.1)
prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n`
addAchievement(user, 'greenCoin', say)
} else {
const miningUpgrades = (user.upgrades.mining || []).map(name => upgrades[name])
diff = miningUpgrades.reduce((total, upgrade) => upgrade.effect(total, user), 1)
prefix = `You mined ${commas(diff)} HVAC.\n`
}
addCoins(user, diff)
return `${prefix}You now have ${commas(user.coins)} HVAC coin${c !== 1 ? 's' : ''}. Spend wisely.`
}
let lbIndex = 0
command(
['!c', '!coin', '!mine', '!'],
'Mine HVAC coins',
async ({ say, user, userId }) => {
await say(await doMine({ user, userId, say }))
if ((lbIndex++) % 20 == 0) {
return updateAllLeaderboards()
}
}
)
command(
['!save'],
'View your savefile',
async ({ say, user }) => {
say('Look, it\'s you! Formatting is ugly because long texts get split up by Slack :[\n```\n' + JSON.stringify(user) + '\n```')
}
)
command(
['!as'],
'Run commands as another user.',
async ({ event, args, trueSay }) => {
const [impersonating, ...newWords] = args
event.user = idFromWord(impersonating)
await getUser(event.user)
const isDisabled = users[event.user].isDisabled
users[event.user].isDisabled = false
event.text = newWords.join(' ')
await messageHandler({ event, say: trueSay, isRecycle: false, skipCounting: true })
users[event.user].isDisabled = isDisabled
}, adminOnly)
command(
['!enable'],
'Enable the given user',
async ({ args }) => {
const user = await getUser(idFromWord(args[0]))
if (user.isDisabled) {
user.isDisabled = false
//saveGame()
addAchievement(user, 'theOtherSide', slack.messageAdmin)
await slack.postToTechThermostatChannel(`_${user.name} has returned..._`)
}
}, adminOnly)
command(
['!disable'],
'Disable the given user',
async ({ args }) => {
const user = await getUser(idFromWord(args[0]))
user.isDisabled = true
}, adminOnly)
command(
['!g', '!gamble'],
'Gamble away your HVAC\n' +
' To use, say \'gamble coin_amount\' or \'!gamble all\'',
async ({ say, trueSay, args, user, event }) => {
if (event.text?.includes(`y'all`)) {
if (event.user !== slack.users.Admin) {
return say('Perhaps another time...')
}
await say(`Gambling the souls of all players...`)
setTimeout(async () => {
say('You bet the souls of your coworkers and won 1 HVAC!')
addCoins(user, 1)
}, 25000)
return
}
const argText = args.join(' ')
const requestedWager = parseAll(argText, user.coins, user)
const n = requestedWager//(chaosFilter(requestedWager, 0.2, user, user.coins) + requestedWager) / 2
if (!n || n < 0) {
return say(`Invalid number '${argText}'`)
}
if (user.coins < n) {
return say(`You don't have that many coins! You have ${commas(user.coins)}.`)
}
if (n >= 100_000_000_000) {
addAchievement(user, 'bigBets', say)
}
if (n >= 100_000_000_000_000) {
addAchievement(user, 'hugeBets', say)
}
if (n >= 100_000_000_000_000_000) {
addAchievement(user, 'mondoBets', say)
}
if (n >= 100_000_000_000_000_000_000) {
addAchievement(user, 'sigmaBets', say)
}
user.coins -= n
let outcome
if (Math.random() > 0.5) {
user.coins += (2 * n)
outcome = 'won'
} else {
outcome = 'lost'
}
console.log(`They ${outcome}`)
//saveGame()
await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`)
if (outcome === 'lost' && user.lostBetMessage) {
await trueSay(user.lostBetMessage)
} else if (outcome === 'won' && user.wonBetMessage) {
await trueSay(user.wonBetMessage)
}
return updateAllLeaderboards()
}
)
const emojiRegex = /^:[^:\s]*:$/
const validEmoji = async emojiText => {
emojiText = emojiText?.trim()
if (!emojiText || !emojiRegex.test(emojiText)) {
return false
}
const validEmojis = (await getEmojis()).emoji
const noColons = emojiText.replace(/:/g, '')
// console.log('validEmojis', validEmojis)
return !!validEmojis[noColons]
}
const getEmojis = async () => await slack.app.client.emoji.list()
command(
['!setloss'],
'!setloss <emoji>',
async ({ args, user, say }) => {
const emoji = args[0]
if (!await validEmoji(emoji)) {
return say(`Argument must be a single emoji!`)
}
user.lostBetMessage = emoji
say(`Set!`)
}, {hidden: true})
command(
['!setwon', '!setwin'],
'!setwon <emoji>',
async ({ args, user, say }) => {
const emoji = args[0]
if (!await validEmoji(emoji)) {
return say(`Argument must be a single emoji!`)
}
user.wonBetMessage = emoji
say(`Set!`)
}, {hidden: true})
command(
['!buynft', '!bn'],
'Acquire high-quality art\n' +
' To use, say \'!buynft nft_name\'',
async ({ event, say, args, user }) => {
const nft = nfts.find(n => n.name.toLowerCase() === args[0])
if (!nft) {
const suffix = args[0]?.match(/[^a-z0-9_]/i) ? '. And I\'m unhackable, so cut it out.' : ''
return say('No NFT with that name found' + suffix)
}
if (nft.owner) {
return say('Someone already owns that NFT!')
}
const c = user.coins
if (c < nft.price) {
return say('You don\'t have enough coin for this nft')
}
user.coins -= nft.price
nft.owner = event.user
//saveGame()
await say('You bought ' + nft.name + '!')
}
)
command(
['!myupgrades', '!myu'],
'List all the upgrades that you own.',
async ({ say, user }) => {
await say(upgradeText(user, true))
}, dmsOnly)
const upgradeText2 = (user, extraMessage = '') => {
const userDoesNotHave = ([upgradeName, upgrade]) => !hasUpgrade(user, upgrade, upgradeName)
const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
return ({
text: (extraMessage && extraMessage + '\n') + upgradeText(user, false),
blocks: [
(extraMessage && {
type: 'section',
text: {
type: 'mrkdwn',
text: extraMessage + '\n'
},
}),
...Object.entries(upgrades)
.filter(userDoesNotHave)
.filter(userMeetsCondition)
.map(([upgradeName]) => upgradeBlock(upgradeName))
].filter(block => block)
})
}
command(
['!upgrade', '!u'],
'Improve the performance of your HVAC-generators.\n' +
' Say \'!upgrade\' to list available upgrades, or \'!upgrade upgrade_name\' to purchase directly.',
async ({ say, args, user }) => {
if (!args[0]) {
return say(upgradeText2(user))
}
console.log({args: args.join(' ')})
const matcher = fuzzyMatcher(args.join(' '))
const u = Object.entries(upgrades).find(([name, upgrade]) => matcher.test(name) || matcher.test(upgrade.name))
if (!u) {
return say(`Could not find an upgrade matching "${args.join(' ')}"!`)
}
const [id, upgrade] = u
if (!user.upgrades[upgrade.type]) {
user.upgrades[upgrade.type] = []
}
if (hasUpgrade(user, upgrade, id)) {
return say(`You already have ${upgrade.name}!`)
}
if (!upgrade.condition(user, getCompletedSquadgradeNames())) {
return say('That item does not exist!')
}
const c = user.coins
if (c < upgrade.cost) {
return say(`You don't have enough coins to buy ${upgrade.name}!\nYou have ${commas(c)}, but you need ${commas(upgrade.cost)}`)
}
user.coins -= upgrade.cost
user.upgrades[upgrade.type].push(id)
//saveGame()
await say(`You bought ${id}!`)
}, dmsOnly)
const upgradeBlock = upgradeName => {
const upgrade = upgrades[upgradeName]
return ({
type: 'section',
text: {
type: 'mrkdwn',
text: `${upgrade.name} :${buyableItems[upgrade.type]?.emoji || upgrade.emoji}: - H${commas(upgrade.cost)}\n_${upgrade.description}_`
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Buy',
emoji: true
},
value: 'upgrade_' + upgradeName,
action_id: 'upgrade_' + upgradeName
}
})
}
const upgradeButton = async ({ body, ack, say, payload }) => {
await ack()
const upgrade = payload.action_id.substring(8)
console.log(`upgradeButton ${upgrade} clicked`)
const event = {
user: body.user.id
}
const user = await getUser(event.user, true)
const words = ['!upgrade', upgrade]
const [commandName, ...args] = words
let extraMessage = ''
say = async text => extraMessage = text
await commands.get('!u').action({ event, say, words, args, commandName, user })
//const highestCoins = user.highestEver || user.coins || 1
await slack.app.client.chat.update({
channel: body.channel.id,
ts: body.message.ts,
...upgradeText2(user, extraMessage)
})
await updateAllLeaderboards()
}
Object.keys(upgrades).forEach(upgradeName => slack.app.action('upgrade_' + upgradeName, upgradeButton))
const getCurrentSquadgrade = () => {
const current = Object.entries(squadUpgrades).find(squadIsMissing)
if (!current) {
return current
}
const [name, upgrade] = current
if (!squad.upgrades[name]) {
squad.upgrades[name] = upgrade.cost
}
return {
name,
upgrade,
remaining: squad.upgrades[name],
emoji: upgrade.emoji,
description: upgrade.description
}
}
const squadText = () => {
const current = getCurrentSquadgrade()
if (current) {
const currentUpgradeText = ({ name, remaining, emoji, description }) =>
`:${emoji}: *${name}* - ${commas(remaining)} HVAC remaining.\n_${description}_`
return currentUpgradeText(current)
}
return 'All squadgrades are unlocked!\n\n' + Object.values(squadUpgrades).map(({ name, cost, emoji, description }) =>
`:${emoji}: *${name}* - ${commas(cost)} HVAC.\n_${description}_`).join('\n\n')
}
command(
['!cat'],
'View your total all-time coins collected',
async ({ say, user }) => {
await say(`You've earned a total of ${commas(user.coinsAllTime)} HVAC`)
}
)
command(
['!squad', '!sq'],
'Buy upgrades that help the whole team.\n' +
' Say !squad to list squad upgrades.\n' +
' Say \'!squad contrib_amount\' to make progress toward the current upgrade.',
async ({ say, args, user }) => {
if (!args[0]) {
return say(squadText())
}
const current = getCurrentSquadgrade()
if (!current) {
return say('No squadgrades are currently available')
}
const currentCoins = user.coins
let amount = parseAll(args.join(' '), currentCoins, user)
if (amount > currentCoins) {
return say(`You don't have that much HVAC! You have ${currentCoins}.`)
}
if (!amount || amount < 1) {
return say(`Invalid amount: '${args[0]}'`)
}
if (amount > squad.upgrades[current.name]) {
amount = squad.upgrades[current.name]
}
squad.upgrades[current.name] -= amount
user.coins -= amount
user.squadGradeContributions ??= 0
user.squadGradeContributions += amount
let status
if (squad.upgrades[current.name] < 1) {
squad.upgrades[current.name] = true
status = `\n\nYou now have ${current.name}!`
} else {
status = ` Current status:\n\n${squadText()}`
}
if (user.squadGradeContributions > 10_000_000_000_000_000) {
//addAchievement(user, '')
}
//saveGame()
await say(`Thank you for your contribution of ${commas(amount)} HVAC!${status}`)
}
)
const { buyRoute, leaderboardUpdater } = require('./buy')
command(
['!buy', '!b', '?b', '?buy'],
'Buy new items to earn HVAC with\n' +
' Use without arguments to list all available items.\n' +
' Say \'!buy item_name optional_quantity\' to make your purchase.\n' +
' Say \'?b item_name optional_quantity\' to check how much your purchase will cost.',
buyRoute
)
command(
['!changelog', '!changes'],
`View my current git log`,
async ({ event, }) => {
const command = `git log > ./gitlog`
const child = exec(command)
child.on('close', async () => {
await slack.app.client.files.upload({
channels: event.channel,
initial_comment: 'Here\'s my current `git log`',
file: createReadStream('./gitlog')
})
})
}
)
command(
['!check', '!ch', '!heck', '!h'],
'Check how many coins another player has',
async ({ say, args, event }) => {
const targetId = idFromWord(args[0])
if (!targetId) {
return say('Target must be a valid @')
}
if (targetId === slack.users.Hvacker) {
const members = (await slack.app.client.conversations.members({ channel: slack.temperatureChannelId })).members
const humanMembers = members.filter(name => name.length === 11)
return say(`Hvacker owns ${humanMembers.length} souls.`)
}
const user = await getUser(targetId)
if (user.isDisabled) {
return say(`<@${targetId}> is no longer with us.`)
}
const fakeC = ''
const coins = event.text[1] === fakeC ? 0 : getCoins(targetId)
await say(`<@${targetId}> has ${commas(coins, args[1] === 'exact')} HVAC.`)
}
)
command(
['!gift', '!give', '!gi'],
'Donate coins to a fellow player\n' +
' Send coins by saying \'!gift @player coin_amount\'',
async ({ event, args, say, user, haunted }) => {
return say(`I'm sorry, but you people can't be trusted anymore.`)
if (haunted) {
return say(`!give doesn't work while you're haunted.`)
}
let [target, ...amountText] = args
amountText = amountText.join(' ')
let targets
if (target === 'everyone') {
targets = Object.entries(users).filter(([id, user]) => id !== event.user && !user.isDisabled && user.name && user.name !== 'Hvacker').map(([id]) => id)
} else {
const targetId = idFromWord(target)
targets = [targetId]
if (targetId === event?.user) {
return say(':thonk:')
}
if (!targetId) {
return say('Target must be a valid @')
}
if (amountText === 'all' && slack.users.Tyler === targetId) {
addAchievement(user, 'walmartGiftCard', say)
}
}
let individualAmount = parseAll(amountText, user.coins, user)
if (individualAmount === user.coins) {
individualAmount = user.coins / targets.length
}
const totalAmount = individualAmount * targets.length
if (!totalAmount || totalAmount < 0) {
return say('Amount must be a positive integer!')
}
if (user.coins < totalAmount) {
return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`)
}
let gifted = []
for (const targetId of targets) {
const targetUser = await getUser(targetId)
user.coins -= individualAmount
if (user.coinsAllTime < 10000) {
// return say('Let \'em play for a bit, ay?')
continue
}
gifted.push(targetId)
targetUser.coins += individualAmount
}
let recipients
if (gifted.length > 1) {
const last = gifted.pop()
recipients = gifted.map(t => users[t].name).join(', ') + ', and ' + users[last].name
} else {
console.log('gifted', gifted)
console.log('users[gifted[0]]', users[gifted[0]])
recipients = users[gifted[0]].name
console.log('recipients', recipients)
}
await say(`Gifted ${commas(individualAmount)} HVAC to ${recipients}`)
}
)
const getChaosMessage = (user, { channel_type }, prefix = '', postfix = '') => {
const userQuackgrades = user.quackUpgrades?.cps || []
if (userQuackgrades.includes('chaos') && channel_type?.endsWith('im')) {
return `${prefix}Current chaos multiplier: ${getChaos(user)}${postfix}`
}
return ''
}
command(
['!status', '!s'],
'Print your current CPS, HVAC balance, and owned items',
async ({ event, say, user }) => {
await say(
`You are currently earning \`${commas(getCPS(user))}\` HVAC Coin per second.\n\n` +
getChaosMessage(user, event, '', '\n\n') +
`You currently have ${commas(user.coins)} HVAC Coins\n\n` +
`${collection(user)}\n\nCoins collected all-time: ${commas(user.coinsAllTime)}\n\n`
)
}
)
command(
['!gimme'],
'Give self x coins',
async ({ say, args, user }) => {
const increase = parseAll(args.join(' '))
addCoins(user, increase)
await say(`You now have ${user.coins} HVAC.`)
}, testOnly)
command(
['!nfts', '!nft', '!n'],
'Show NFTs in the museum\n' +
' Call with no arguments to list all NFTs, or \'!nft nft_name\' to show a particular one.',
async ({ say, args }) => {
const owner = nft => `Owner: *${slack.users[nft.owner] || 'NONE'}*`
const nftDisplay = nft => `_"${nft.name}"_\n\n${nft.description}\n\n${commas(nft.price)} HVAC.\n\n${nft.picture}\n\n${owner(nft)}`
const matcher = fuzzyMatcher(args[0] || '')
await say(nfts
.filter(({name}) => matcher.test(name))
.map(nftDisplay)
.join('\n-------------------------\n') || (args[0] ? 'No matching NFTs found' : 'No NFTs currently exist.')
)
}
)
let emojiLevel = 1
const buildPEmoji = name => {
const ret = [name, emojiLevel]
emojiLevel *= 2
return ret
}
const prestigeEmojis = [
'rock',
'wood',
'seedling',
'evergreen_tree',
'hibiscus',
'thunder_cloud_and_rain',
'rainbow',
'star',
'dizzy',
'sparkles',
'star2',
'stars',
'comet',
'night_with_stars',
'milky_way',
'eye',
].map(buildPEmoji)
const prestigeEmoji = user => {
const p = user.prestige || 0
if (!p) {
return ''
}
let e = ''
for (const [emoji, requiredLevel] of prestigeEmojis) {
if (p < requiredLevel) {
break
}
e = emoji
}
return e && `:${e}:`
}
command(
['!pemojis', '!pemoji'],
`Show the emoji for each prestige level you've been through.`,
async ({ say, user, event, args }) => {
let p = user.prestige || 0
if (event.user === slack.users.Admin && args[0] === 'all') {
p = 99999999
}
let message = ''
for (const [emoji, requiredLevel] of prestigeEmojis) {
if (requiredLevel > p) {
break
}
message += `${requiredLevel} => :${emoji}:\n`
}
return say(message)
}, prestigeOnly)
command(
['!pet'],
`Take care of the office pet!\nPet bonuses are shared between all players!`,
async ({ say, user, event, args }) => {
pets.petToText(pet, null, say)
})
const strike = user => user.isDisabled ? '~' : ''
const struck = (user, string) => strike(user) + string + strike(user)
const generateLeaderboard = ({ args }, showCat) => {
let index = 1
return Object.entries(users)
.filter(([, user]) => (!user.isDisabled || args.includes('all')) && user.name && !['Hvacker', '???', 'TEST-USER'].includes(user.name))
.sort(([id, user1], [id2, user2]) => {
const leftPrestige = getUserSync(id).prestige
const rightPrestige = getUserSync(id2).prestige
if (leftPrestige > rightPrestige) {
return -1
}
if (rightPrestige > leftPrestige) {
return 1
}
return getCPS(user1) > getCPS(user2)
})
.map(([id, u]) => `${strike(u)}${index++}. ${slack.users[id] || '???'} ${prestigeEmoji(u) || '-'} ${commas(getCPS(getUserSync(id)))} CPS - ${commas(getCoins(id))} HVAC${showCat ? ` (${commas(users[id].coinsAllTime)} all-time)` : ''}${strike(u)}`)
.join('\n')
}
command(
['!leaderboard', '!lb'],
'Show the top HVAC-earners, ranked by prestige, then CPS',
async ({ say, user, args, event }) => {
// if ((event.user === slack.users.Houston || event.user === slack.users.Admin) && event.channel_type.includes('im')) {
// return say('```' + `Hvacker - 9 souls - Taking from them whatever it desires\nSome other losers - who cares - whatever` + '```')
// }
game.leaderboardChannels ??= {}
const showCat = event.user === slack.users.Admin && args.includes('cat')
await say(generateLeaderboard({ args }, showCat)).then(({ channel, ts }) => {
addAchievement(user, 'leaderBoardViewer', say)
if (args.includes('all') || showCat) {
return
}
game.leaderboardChannels[channel] = ts
return updateAllLeaderboards({ channel, ts })
})
}
)
const updateAllLeaderboards = async (add) => {
const currentBoard = generateLeaderboard({ args: [] })
return updateAll({ name: 'leaderboard', text: currentBoard, add })
}
leaderboardUpdater.updateAllLeaderboards = updateAllLeaderboards
const hints = []
const oneShot = (name, helpText, message, achievementName) => {
if (helpText) {
hints.push(helpText)
}
const names = Array.isArray(name) ? name : [name]
command(names, helpText, async ({ say, user }) => {
await say(message)
if (achievementName) {
addAchievement(user, achievementName, say)
}
}, { hidden: true })
}
const oneShotFile = async (name, helpText, text, filePath, achievementName) => {
const names = Array.isArray(name) ? name : [name]
command(names, helpText, async ({ event }) => {
await slack.app.client.files.upload({
channels: event.channel,
initial_comment: text,
file: createReadStream(filePath)
})
if (achievementName) {
addAchievement(user, achievementName, say)
}
}, { hidden: true })
}
oneShot('!peter-griffin-family-guy', 'Good stuff great comedy.', `Are you sure?\nThis will permanently delete all of your progress and remove you from the game.\nSay !!peter-griffin-family-guy to confirm.`)
oneShotFile('!santa', 'Ho ho ho!', '<https://i.imgur.com/dBWgFfQ.png|I\'m Santa Quacks>')
oneShot('!sugma', 'Not very original.', ':hvacker_angery:')
oneShotFile('!pog', 'One poggers hvacker', 'poggers', 'images/XCg7WDz.png')
oneShotFile('!ligma', 'Not very original.', 'no', 'images/i1YtW7m.png')
oneShotFile(['!dab', '!dabs'], 'ACTIVATE COOL GUY MODE', 'I go XD style', 'images/FKYdeqo.jpg', 'certifiedCoolGuy')
oneShotFile('!based', 'Sorry, it\'s a little hard to hear you!', 'What?', 'images/IUX6R26.png')
oneShotFile('!shrek', 'Is love and is life.', 'Donkey!', 'images/QwuCQZA.png')
oneShotFile('!sugondese', 'I don\'t like you.', 'rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr', 'images/VCvfvdz.png')
oneShot('!bofa', 'bofa deez yes yes we get it', ':goorab:')
oneShotFile('!squeedward', 'Who lives in an easter island head, under the sea?', 'Squeedward', 'images/squeedward.png')
// oneShot('!imaginedragons', 'The worst part about any group of white people is that they could be Imagine Dragons and you\'d have no way of knowing.', '<https://i.imgur.com/QwuCQZA.png|Donkey!>')
command(
['!hint'],
'',
async ({ say }) => say(`_${hints[Math.floor(Math.random() * hints.length)]}_`)
, {hidden: true})
command(
['!addnft'],
`Arguments 1 and 2 should be on the first line as name (one word!) and integer price.
The second line should be the description of the pieces
the picture is everything after the second line`,
async ({ event }) => {
let [, name, ...price] = event.text.substring(0, event.text.indexOf('\n')).split(' ')
const rest = event.text.substring(event.text.indexOf('\n') + 1)
const desc = rest.substring(0, rest.indexOf('\n'))
const picture = rest.substring(rest.indexOf('\n') + 1)
price = price.join(' ')
const newNft = {
name,
price: parseInt(price.replace(/,/g, '')),
description: desc,
picture,
owner: null
}
nfts.push(newNft)
console.log('addedNft', newNft)
}, adminOnly)
command(
['!prestige', '!p'],
'Show your current prestige status',
prestige.prestigeRoute
)
command(
['!!prestige'],
'Confirm your prestige activation.',
prestige.prestigeConfirmRoute, { hidden: true })
command(
['!pbeta'],
'Confirm your prestige activation.',
async ({ event, say, user, YEET }) => {
await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
}, { hidden: true })
command(
['!quack', '!quackstore'],
'Confirm your prestige activation.',
async ({ event, say, user, YEET }) => {
await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
}, prestigeOnly)
// command(
// ['!quack', '!quackstore'],
// 'Spend your prestigious quackings\n\n' +
// 'Say \'!quack upgrade_name\' to purchase a quackgrade',
// prestige.quackStoreRoute,
// prestigeOnly
// )
command(
['!myquack', '!myq'],
'See what kind of quackery you own.',
prestige.ownedQuacksRoute,
prestigeOnly
)
command(
['!lore', '!l'],
'Sit down for a while. Maybe learn something.\n' +
'You can use `!lore reset` to return to the beginning.',
lore,
dmsOnly
)
command(
['!ss'],
'Show the status for another player: !ss @player',
async ({ args, say }) => {
const target = args[0]
const targetId = idFromWord(target)
const targetUser = await getUser(targetId)
await say(
`${target} is currently earning \`${commas(getCPS(targetUser))}\` HVAC Coin per second.\n\n` +
`They have ${commas(getCoins(targetId))} HVAC Coins\n\n` +
`${collection(targetUser)}\n\n` +
`${Object.entries(targetUser?.items || {}).reduce((total, [, count]) => total + count, 0)} total items\n\n`
)
}, adminOnly)
command(
['!steal', '!sagesteal'],
'Highly illegal',
async ({ event, args, say, user }) => {
const [target] = args
const amount = user.coins / 100
if (!amount || amount < 0) {
return
}
let targetId = idFromWord(target)
console.log({ user: event.user, target, targetId })
if (event.user === targetId) {
return say('What, are you trying to steal from yourself? What, are you stupid?')
}
if (!targetId) {
targetId = slack.users.Admin
}
if (user.coins < amount) {
return
}
const targetUser = await getUser(targetId)
user.coins -= amount
targetUser.coins += amount
await say(`Stealing is wrong. Gave ${commas(amount)} of your HVAC to ${slack.users[targetId]}`)
}, { hidden: true })
command(
['!lotto'],
'Once per day, try for a big win!',
async ({ event, say, user }) => {
const currentDate = new Date().getDate()
const lastLotto = user.lastLotto === undefined ? currentDate - 1 : user.lastLotto
if (lastLotto === currentDate && event.user !== slack.users.Admin) {
return say('Hey, only one lotto ticket per day, alright?')
}
let msg
if (Math.random() < 0.01) {
const prize = 5000 + (user.coinsAllTime / 20)
msg = `Ayyyyy, we have a winner! You win ${commas(prize)} HVAC!`
addCoins(user, prize)
} else {
msg = `Sorry pal, today's not your lucky day.`
}
await say(msg)
user.lastLotto = currentDate
//saveGame()
}, { hidden: true })
command(
['!ignite'],
'You found me!',
async ({ say, user }) => {
addAchievement(user, 'ignited', say)
}, { hidden: true })
command(
['!giveach'],
'!giveach @player ach_name',
async ({ args, say, }) => {
addAchievement(await getUser(idFromWord(args[0])), args[1], say)
//saveGame()
}, adminOnly)
command(
['!whois'],
'!whois player_id',
async ({ args, say }) => say(`<@${args[0]}>`),
adminOnly)
command(
['!!postas'],
'',
async ({ args }) => {
let channel = slack.temperatureChannelId
if (args[0].startsWith('<#')) {
channel = args[0].substring(2, args[0].indexOf('|'))
const [, ...rest] = args
args = rest
}
if (args[0].startsWith('<@')) {
channel = args[0].substring(2, args[0].length - 1)
const [, ...rest] = args
args = rest
}
console.log({args, channel})
const target = idFromWord(args[0])
const [, ...rest] = args
const userInfo = await slack.app.client.users.info({
user: target
})
console.log(userInfo)
return slack.app.client.chat.postMessage({
channel,
text: rest.join(' '),
username: userInfo.user.profile.real_name,
icon_url: userInfo.user.profile.image_original
})
},
adminOnly)
command(
['!!startpoll'],
'',
async ({ args, say }) => {
},
adminOnly)
command(
['!ngift'],
'!ngift player_id nft_name',
async ({ event, args, say, haunted }) => {
if (haunted) {
return say(`!ngift doesn't work while you're haunted.`)
}
const targetId = idFromWord(args[0])
if (!targetId) {
return say('Please specify a valid @ target!')
}
const nft = nfts.find(nft => nft.name === args[1])
if (!nft) {
return say(`There is not NFT named "${args[1]}"!`)
}
if (nft.owner !== event.user) {
return say(`You do not own "${nft.name}"!`)
}
nft.owner = targetId
}, { hidden: true })
command(
['!deletetest', '!dtest'],
'!deletetest',
async () => {
delete users[slack.testId]
//saveGame()
}, adminOnly)
const stonkPatterns = {
// Change %: ~1.25
duk: [
2.36, -1.69, -1.93, 4.95, -0.99, -0.89,
0.90, 2.21, 1.29, -4.93, 1.90, 0.90,
0.37, -0.07, 2.85, 0.25, 0.40, 1.58,
3.10, 1.37, 0.68, -2.57, -1.80, -0.21,
-2.80, -0.11, 0.31, -0.25, 1.56, 1.97,
-0.44, -6.28, 2.67, 2.85, 5.37, 2.04,
3.01, 1.75, -3.53, 0.38, -7.63, -1.89,
-0.83, -2.27, 6.14, -2.05, 0.84, -3.22,
2.34, -1.79, 1.26, -2.48, -5.73, 0.37,
-4.63, 5.31, 1.23
],
// Change %: ~13.30
quak: [
3.21, -0.81, 0.84, 0.17, 2.18, -0.19,
-2.57, 1.26, 0.44, -2.28, 0.11, -0.21,
1.16, 4.07, -0.28, 0.61, 1.76, -0.90,
1.14, -2.37, 0.96, -2.35, -3.42, -1.21,
0.00, 2.10, -0.18, 3.42, -1.71, -0.32,
-1.77, -1.46, 0.87, 0.60, 1.63, 1.51,
-0.07, 1.87, -0.38, -0.44, -1.02, 1.70,
-0.46, -4.32, 0.06, 0.41, 5.03, 0.84,
-1.03, 3.88, 3.38, 2.24, -0.43, -0.50,
-3.61, 0.32
],
// Change %: ~0.77
honk: [
-0.30, 3.73, 11.27, -7.71, -6.81, 1.15,
3.55, -3.42, 8.51, -3.22, 8.20, 0.19,
0.76, -2.00, 9.63, 0.87, 3.14, -3.76,
-2.27, 3.42, 2.39, 4.51, -0.35, -0.95,
-6.64, -6.88, 8.90, 0.42, -0.04, -3.33,
1.85, 4.16, -5.26, -7.24, 5.35, 0.46,
5.16, -0.10, -5.24, 5.34, -8.52, 6.17,
-0.80, 2.92, -2.21, -6.80, -2.22, 10.16,
-1.63, -10.11, 6.88, -4.19, -8.53, -2.68,
-7.49, 5.70, 1.23, -1.0
],
}
const nextPattern = pattern => {
switch (pattern) {
case "duk":
return "quak"
case "quak":
return "hvac"
default:
return "duk"
}
}
const updateStonkPrices = () => {
const today = daysSinceEpoch()
if (stonkMarket.lastDay === today) {
return // already updated
}
// TODO: Gotta take into account wrapping around to the end of the year
Object.entries(stonkMarket.stonks).forEach(([, stonk]) => {
console.log(stonk.pattern)
console.log('try set')
for (let i = stonkMarket.lastDay; i < today; i++) {
console.log('set lastPrice')
stonk.lastPrice = stonk.price
console.log(stonk.pattern, stonkPatterns)
stonk.price *= 1 + ((stonkPatterns[stonk.pattern] || stonkPatterns.duk)[stonk.index] / 100)
stonk.index++
if (stonk.index >= stonkPatterns[stonk.pattern]?.length) {
stonk.index = 0
stonk.pattern = nextPattern(stonk.pattern)
}
}
})
stonkMarket.lastDay = today
//saveGame(null, true)
}
const buyStonks = (user, stonkName, quantityPhrase) => {
const quantity = parseAll(quantityPhrase, Math.floor(user.coins / stonkMarket.stonks[stonkName].price), user)
if (!quantity) {
return 'Quantity must be positive integer!'
}
const cost = stonkMarket.stonks[stonkName].price * quantity
if (cost > user.coins) {
return `Buying ${commas(quantity)} of ${stonkName.toUpperCase()} would cost ${commas(cost)} HVAC. You only have ${commas(user.coins)}!`
}
user.coins -= cost
user.holdings ??= {}
user.holdings[stonkName] ??= 0
user.holdings[stonkName] += quantity
//saveGame()
return `Successfully bought ${commas(quantity)} ${stonkName.toUpperCase()}, for a total of ${commas(cost)} HVAC!`
}
const sellStonks = (user, stonkName, quantityPhrase) => {
user.holdings ??= {}
user.holdings[stonkName] ??= 0
const quantity = parseAll(quantityPhrase, user.holdings[stonkName], user)
if (!quantity) {
return 'Quantity must be positive integer!'
}
if (quantity > user.holdings[stonkName]) {
return `You're trying to sell ${commas(quantity)} ${stonkName.toUpperCase()}, but you only have ${user.holdings[stonkName]}!`
}
const sellValue = stonkMarket.stonks[stonkName].price * quantity
user.holdings[stonkName] -= quantity
user.coins += sellValue
//saveGame()
return `You successfully sold ${commas(quantity)} ${stonkName.toUpperCase()}, for a total of ${commas(sellValue)} HVAC!`
}
const stonkHelp =
'Play the stonk market. Prices change every day!\n' +
'`!stonk buy <name> <quantity>`\n' +
'`!stonk sell <name> <quantity>`'
command(
['!stonks', '!stonk', '!st'],
stonkHelp,
async ({ user, args, say }) => {
return say(`Stonks are busted atm but thank you for trying!`)
updateStonkPrices()
let msg = `Market values:\n`
Object.entries(stonkMarket.stonks).forEach(([name, stonk]) => {
const diff = stonk.price - stonk.lastPrice
const diffPercent = diff / stonk.lastPrice
const diffSign = diff > 0 ? '+' : ''
msg += `\`${name.toUpperCase()}\` ${commas(stonk.price)} (${diffSign}${diffPercent.toPrecision(2)}%)\n`
})
let [action, stonkName, ...quantityPhrase] = args
const noHoldingsMessage = msg + `\nYou don't have any holdings right now.`
if (!action) {
if (!user.holdings) {
return say(noHoldingsMessage)
}
msg += `\nYou have:`
let hasHoldings = false
Object.entries(user.holdings).forEach(([name, holdings]) => {
if (holdings > 0) {
hasHoldings = true
msg += `\n${commas(holdings)} ${name.toUpperCase()} - ${commas(stonkMarket.stonks[name].price * holdings)} HVAC`
}
})
if (!hasHoldings) {
return say(noHoldingsMessage)
}
return say(msg)
}
action = action.toLowerCase()
stonkName = stonkName.toLowerCase()
quantityPhrase = quantityPhrase?.join(' ') || '1'
if (action === 'buy' || action === 'b') {
return say(buyStonks(user, stonkName, quantityPhrase))
} else if (action === 'sell' || action === 's') {
return say(sellStonks(user, stonkName, quantityPhrase))
} else {
return say(stonkHelp)
}
}
)
command(
['!speak'],
'!speak <64-character message>',
async ({ event, say, user }) => {
const today = daysSinceEpoch()
if (user.lastSpeech === today) {
return say(`Sorry, one speech per day, kid.`)
}
if (event.text.length > 64 + '!speak '.length) {
return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
}
if (event.text.length > 64 + '!speak '.length) {
return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
}
if (event.text.includes('<@')) {
return say(`Please don't @ people when using my voice, you're just gonna get me in trouble.`)
}
user.lastSpeech = today
const message = event.text.substring(7)
await slack.postToTechThermostatChannel(message)
//saveGame(null, true)
}, {
hidden: true,
condition: ({ user }) => userHasCheckedQuackgrade(user, 'theVoice')
})
command(
['!message', '!msg', '!m'],
'!message player_id message',
async ({ event, args, say }) => {
const targetId = idFromWord(args[0])
if (!targetId) {
return say('Please specify a valid @ target!')
}
const msg = event.text.replace(/\S+ +\S+\s*/i, '')
await slack.messageIn(targetId, msg)
}, adminOnly)
command(
['!userjson'],
'Fetch the raw JSON data for your user',
async ({ user, trueSay }) => trueSay('```\n' + JSON.stringify(user) + '\n```'),
{ hidden: true }
)
command(
['!whohas'],
'!whohas <achievement name>',
async ({ user, say, args }) => {
const achName = args.join(' ')
const matcher = fuzzyMatcher(achName)
const achievement = Object.entries(achievements)
.find(([name, ach]) => [name, ach.name].some(n => matcher.test(n)))
if (!achievement || !user.achievements[achievement[0]]) {
return say(`You don't have any achievement matching '${achName}'`)
}
const [achId, ach] = achievement
const owners = Object.entries(users)
.map(([, potentialOwner]) => potentialOwner)
.filter(potentialOwner => potentialOwner.achievements[achId])
.map(owner => owner.name)
return say(`${owners.length} ${owners.length === 1 ? 'user has' : 'users have'} _${ach.name}_ :${ach.emoji}:\n${owners.join('\n')}`)
},
{ hidden: true }
)
const manageReactions = async ({ args, action }) => {
const [link, ...reactions] = args
const split = link.replace(/.*archives\//, "").split("/")
const channelId = split[0]
// split[1] ~= 'p1234567890123456'
const t = split[1].substring(1).replace(/[^0-9]/g, "")
const timestamp = t.substring(0, t.length - 6) + '.' + t.substring(t.length - 6)
return action({
app: slack.app,
channelId,
timestamp,
reactions: reactions.map(react => react.replace(/:/gi, ""))
})
}
command(
['!react', '!addreact'],
'!addreact <link> <emojis>',
async ({ args }) => {
return manageReactions({ args, action: addReactions })
},
adminOnly
)
command(
['!rmreact'],
'!rmreact <link> <emojis>',
async ({ args }) => {
return manageReactions({ args, action: removeReactions })
},
adminOnly
)
webapi.launch()
logMemoryUsage()
module.exports = {
command,
adminOnly
}