const { saveGame, logError, getCPS, squadUpgrades, getItemCps, squadIsMissing, idFromWord, getCoins, getUser, commas, addAchievement, shufflePercent, parseAll, getRandomFromArray, chaosFilter, addReactions, setHighestCoins, definitelyShuffle, getCompletedSquadgradeNames, setKnownUsers, dayOfYear, daysSinceEpoch, userHasCheckedQuackgrade, fuzzyMatcher, addCoins, game, updateAll } = require('./utils') const { nfts, squad, users, horrors, stonkMarket, pet } = game const pets = require('./gotcha') 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 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 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 } 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( ['!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' ] if (settings.horrorEnabled) { command( ['!horror'], 'help help help help help', async ({ event, say }) => { 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?_') // TODO horror horrors change help to !help }, { hidden: true }) } const buildHorrorSay = ({ say, event, commandName, c }) => async message => { const punishmentOffset = event.user === slack.users.Quade ? 0 : 0 const shuffleOdds = getShuffleOdds(punishmentOffset + 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 //saveGame() 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 cursedPics = { U0B8W0AF3: ['https://i.imgur.com/UNYKS0c.png'], //'Charles', //U0B8RTK5L: '', //'Zane', U02KYLVK1GV: ['https://i.imgur.com/VTSob8w.png', 'https://i.imgur.com/BdXqA5d.jpeg'], //'Quade', UTDLFGZA5: ['https://i.imgur.com/YIrz4JQ.png'], // 'Tyler', U017PG4EL1Y: ['https://i.imgur.com/d65EuaQ.png'],//'Max', // hole viscera U02AAB54V34: ['https://i.imgur.com/ewT3AoL.png', 'https://i.imgur.com/LLyzybV.png'] // 'Houston' } const messageHandler = async ({ event, say, isRecycle = false, skipCounting }) => { 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) let user = getUser(event.user) if (user.isDisabled) { 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 = 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 (cursedPics[event.user]?.length > 0) { icon_url = getRandomFromArray(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` }) } } } 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 } 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 endTime = new Date() saveGame(`command ${event.text} finished in ${endTime - startTime}ms`) } 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: 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: getUser(player), // ms: 30000, // channel: player // }) // }) const targetId = idFromWord(args[0]) for (let i = 0; i < 10; i++) { setTimeout(async () => await lightning({ user: 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 = 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] saveGame('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(userId => { 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]) //saveGame() 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() //saveGame() 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(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: 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) console.log({ miningUpgrades, diff, user: user.upgrades.mining }) prefix = `You mined ${commas(diff)} HVAC.\n` } addCoins(user, diff) //saveGame() 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++) % 5 == 0) { return updateAllLeaderboards() } } ) command( ['!as'], 'Run commands as another user.', async ({ event, args, trueSay }) => { const [impersonating, ...newWords] = args event.user = idFromWord(impersonating) 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( ['!react'], '!react ', async ({ args }) => { const [emoji, timestamp] = args console.log('args:', args) try { await slack.app.client.reactions.add({ channel: slack.temperatureChannelId, timestamp, name: emoji.replace(/:/g, '') }) } catch (e) { console.error('!react error', e) } }, adminOnly) command( ['!enable'], 'Enable the given user', async ({ args }) => { const user = 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 = 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 = (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 }, {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 }, {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 = 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 'No more squadgrades currently available.' } 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 = 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 ({ args, say, user, haunted }) => { if (haunted) { return say(`!give doesn't work while you're haunted.`) } let [target, ...amountText] = args amountText = amountText.join(' ') const amount = parseAll(amountText, user.coins, user) const targetId = idFromWord(target) if (!amount || amount < 0) { return say('Amount must be a positive integer!') } if (!targetId) { return say('Target must be a valid @') } if (user.coins < amount) { return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`) } if (amountText === 'all' && slack.users.Tyler === targetId) { addAchievement(user, 'walmartGiftCard', say) } const targetUser = getUser(targetId) user.coins -= amount targetUser.coins += amount await say(`Gifted ${commas(amount)} HVAC to <@${targetId}>`) } ) 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 generateLeaderboard = ({ args }) => { let index = 1 return Object.entries(users) .filter(([, user]) => (!user.isDisabled || args[0] === 'all') && (Object.entries(user.items).length > 0 || user.prestige)) .sort(([id, user1], [id2, user2]) => { const leftPrestige = getUser(id).prestige const rightPrestige = getUser(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(getUser(id)))} CPS - ${commas(getCoins(id))} HVAC${strike(u)}`) .join('\n') } command( ['!leaderboard', '!lb'], 'Show the top HVAC-earners, ranked by prestige, then CPS', async ({ say, user, args }) => { // 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 ??= {} await say(generateLeaderboard({ args })).then(({ channel, ts }) => { addAchievement(user, 'leaderBoardViewer', say) 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) await slack.messageAdmin(`Wow buddy they like your ${name} joke.`) 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.`) oneShot('!santa', 'Ho ho ho!', '') oneShot('!sugma', 'Not very original.', ':hvacker_angery:') oneShot('!pog', 'One poggers hvacker', '') oneShot('!ligma', 'Not very original.', '') oneShot(['!dab', '!dabs'], 'ACTIVATE COOL GUY MODE', '', 'certifiedCoolGuy') oneShot('!based', 'Sorry, it\'s a little hard to hear you!', '') oneShot('!shrek', 'Is love and is life.', '') oneShot('!sugondese', 'I don\'t like you.', '') // 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.\n' + ' The second line should be the description of the pieces\n' + ' 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 = 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) const exec = require('child_process').exec const { createReadStream } = require('fs') command( ['!pl'], '!pl `code`\n\n' + 'A very very stupid lisp implementation.', async ({ event, say }) => { return let code = ' (def sys 0)'// = '(def iLoveHvacker "9jklFUlbnd38bCrrU9765FhN") ' code += event.text.substring(4) .replace(/`/g, '') .replace(/</g, '<') code += ' ' // console.log('PL CODE:', code) //const fileName = '/home/sagevaillancourt/git/hvacker/pl/' + event.user + (new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_')) //fs.writeFileSync(fileName, code) const command = `/home/sagevaillancourt/projects/pebblisp/src/pl '${code}'` console.log('pl command:', command) const child = exec(command) let result = '```\n' let errors = '' child.stdout.on('data', data => (result += data.replace(/\[\d+m/g, ''))) child.stderr.on('data', data => { result += `\nERR:${data}\n` errors += `\nERR:${data}` }) const maxLines = 25 const maxChar = 2000 child.on('close', async () => { // fs.rmSync(fileName) const lines = result.split('\n') let err = '\n' if (lines.length > maxLines) { result = lines.slice(0, maxLines).join('\n') + '\n...' err += `Output exceeds ${maxLines} lines and was truncated.\n` } if (result.length > maxChar) { result = result.substring(0, maxChar) err += `\nOutput also exceeds ${maxChar} characters and was truncated further...\n` } result += '```' + err + (errors && 'stderr:\n```\n' + errors.substring(0, 200) + '```') await say(result) }) }, 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) 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 = 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(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][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 }) => { 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(JSON.stringify(user)), { 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(matcher.test)) 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 } ) command( ['!user-list'], 'Lists all users', async ({ say }) => { const users = await slack.app.client.users.list() console.log(users.members.filter(m => m.real_name?.toLowerCase().includes('nik'))) say('k') }, adminOnly ) //webapi.launch() module.exports = { command, adminOnly }