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 `! 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 ', 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 ', 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 ', 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 ', 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 ', 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 ', 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 ', 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!', '') 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.', '') 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 `\n' + '`!stonk sell `' 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 ', 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 ', async ({ args }) => { return manageReactions({ args, action: addReactions }) }, adminOnly ) command( ['!rmreact'], '!rmreact ', async ({ args }) => { return manageReactions({ args, action: removeReactions }) }, adminOnly ) webapi.launch() logMemoryUsage() module.exports = { command, adminOnly }