diff --git a/src/auth/index.js b/src/auth/index.js index c671447..a35a75a 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -6,8 +6,9 @@ const url = '' const headers = new Headers() headers.append('Authorization', 'Basic ' + base64.encode(config.honeywellKey + ':' + config.honeywellSecret)) -fetch(url, {method:'GET', - headers: headers, - //credentials: 'user:passwd' +fetch(url, { + method: 'GET', + headers: headers + // credentials: 'user:passwd' }).then(response => response.json()) - .then(json => console.log(json)); \ No newline at end of file + .then(json => console.log(json)) diff --git a/src/config.js b/src/config.js index bed7533..b47b4f1 100644 --- a/src/config.js +++ b/src/config.js @@ -5,17 +5,17 @@ const fs = require('fs') const fileName = 'hvackerconfig.json' const configPaths = [ - path.join('/secrets', fileName), - path.join(homedir, '.add123', fileName) + path.join('/secrets', fileName), + path.join(homedir, '.add123', fileName) ] const getConfigData = () => { - const configPath = configPaths.find(fs.existsSync) - if (!configPath) { - throw 'Could not find a config file!' - } - const config = fs.readFileSync(configPath, 'utf8') - return JSON.parse(config) + const configPath = configPaths.find(fs.existsSync) + if (!configPath) { + throw 'Could not find a config file!' + } + const config = fs.readFileSync(configPath, 'utf8') + return JSON.parse(config) } -module.exports = getConfigData() \ No newline at end of file +module.exports = getConfigData() diff --git a/src/games/connect4.js b/src/games/connect4.js index fcfb381..7f376e6 100644 --- a/src/games/connect4.js +++ b/src/games/connect4.js @@ -1,12 +1,12 @@ const routine = require('./routine') const emptyBoard = [ - [' ', ' ', ' ', ' ', ' ', ' ', ' ',], - [' ', ' ', ' ', ' ', ' ', ' ', ' ',], - [' ', ' ', ' ', ' ', ' ', ' ', ' ',], - [' ', ' ', ' ', ' ', ' ', ' ', ' ',], - [' ', ' ', ' ', ' ', ' ', ' ', ' ',], - [' ', ' ', ' ', ' ', ' ', ' ', ' ',] + [' ', ' ', ' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' ', ' ', ' '] ] const textFromBoard = board => { @@ -30,7 +30,7 @@ const checkRows = board => { const checkColumns = board => { const colToText = i => - board[0][i] + + board[0][i] + board[1][i] + board[2][i] + board[3][i] + @@ -54,7 +54,7 @@ const checkDiagonals = board => { board[row][col] === board[row + 1][col + 1] && board[row][col] === board[row + 2][col + 2] && board[row][col] === board[row + 3][col + 3] - ){ + ) { return board[row][col] } } @@ -65,7 +65,7 @@ const checkDiagonals = board => { board[row][col] === board[row + 1][col - 1] && board[row][col] === board[row + 2][col - 2] && board[row][col] === board[row + 3][col - 3] - ){ + ) { return board[row][col] } } @@ -110,7 +110,7 @@ const placeAt = (i, board, char) => { } const makeMove = (emoji, board) => - placeAt(numEmojis.indexOf(emoji), board, getTurn(board)) + placeAt(numEmojis.indexOf(emoji), board, getTurn(board)) routine.build({ startTriggers: ['connect 4', 'c4'], @@ -120,4 +120,4 @@ routine.build({ textFromBoard, checkWinner: board => checkRows(board) || checkColumns(board) || checkDiagonals(board) || checkFull(board), makeMove -}) \ No newline at end of file +}) diff --git a/src/games/hvacoins/achievements.js b/src/games/hvacoins/achievements.js index 12f6612..e2b0430 100644 --- a/src/games/hvacoins/achievements.js +++ b/src/games/hvacoins/achievements.js @@ -2,12 +2,12 @@ module.exports = { leaderBoardViewer: { name: 'Leaderboard-Viewer', description: 'Thank you for viewing the leaderboard!', - emoji: 'trophy', + emoji: 'trophy' }, seeTheQuade: { name: 'See the Quade', description: 'Quade has appeared in your buyables', - emoji: 'quade', + emoji: 'quade' }, greenCoin: { name: 'Lucky Green Coin', @@ -46,7 +46,7 @@ module.exports = { emoji: 'mouse2' }, weAllNeedHelp: { - name: `View the '!coin' help`, + name: 'View the \'!coin\' help', description: 'We all need a little help sometimes', emoji: 'grey_question' }, @@ -71,9 +71,19 @@ module.exports = { description: 'You absolutely know how to party.', emoji: 'sunglasses' }, + itsOverNineHundred: { + name: 'Play the HVAC game 1000 times', + description: 'It\'s over nine hundred and ninety-nine!', + emoji: 'chart_with_upwards_trend' + }, youDisgustMe: { name: 'You disgust me', description: 'Like, wow.', emoji: 'nauseated_face' + }, + bookWorm: { + name: 'Take a peek at the lore', + description: 'It\'t gotta be worth your time somehow.', + emoji: 'books' } } diff --git a/src/games/hvacoins/buy.js b/src/games/hvacoins/buy.js index 796509d..762de0d 100644 --- a/src/games/hvacoins/buy.js +++ b/src/games/hvacoins/buy.js @@ -1,5 +1,5 @@ -const buyableItems = require('./buyableItems'); -const { commas, saveGame, setHighestCoins, addAchievement, getCoins, getUser, singleItemCps } = require('./utils'); +const buyableItems = require('./buyableItems') +const { commas, saveGame, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter } = require('./utils') const slack = require('../../slack') const calculateCost = ({ itemName, user, quantity = 1 }) => { @@ -29,37 +29,71 @@ const buildBlock = ({ user, itemName, cost, cps }) => ({ type: 'section', text: { type: 'mrkdwn', - text: `${itemName} :${buyableItems[itemName].emoji}:x${user.items[itemName] || 0} - 𝕳${commas(cost)} - ${commas(cps)} CPS\n_${buyableItems[itemName].description}_` + text: `${itemName} :${buyableItems[itemName].emoji}:x${user.items[itemName] || 0} - H${commas(cost)} - ${commas(cps)} CPS\n_${buyableItems[itemName].description}_` }, accessory: { type: 'button', text: { type: 'plain_text', - text: 'Buy 1', + text: '1', emoji: true }, value: 'buy_' + itemName, action_id: 'buy_' + itemName - }, + } +}) + +const buildBlock2 = ({ user, itemName, cost, cps }) => ({ + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'Buy 1', + emoji: true + }, + value: 'buy_' + itemName, + action_id: 'buy_' + itemName + }, + { + type: 'button', + text: { + type: 'plain_text', + text: 'Buy 1', + emoji: true + }, + value: 'buy_' + itemName, + action_id: 'buy_' + itemName + } + ] }) const buyText2 = (highestCoins, user) => { return ({ text: buyableText(highestCoins, user), blocks: Object.entries(buyableItems) - .filter(canView(highestCoins)) - .map(([itemName, item]) => { - const cost = calculateCost({ itemName, user, quantity: 1 }) - const cps = Math.round(singleItemCps(user, itemName)) - return ({ user, itemName, cost, cps }) - }).map(buildBlock) + .filter(canView(highestCoins)) + .map(([itemName, item]) => { + const cost = calculateCost({ itemName, user, quantity: 1 }) + const cps = Math.round(singleItemCps(user, itemName)) + return ({ user, itemName, cost, cps }) + }).map(buildBlock) }) } -const buyRoute = async ({ event, say, words }) => { - const user = getUser(event.user) +const maxQuantity = ({ itemName, user, currentCoins }) => { + let quantity = 1 + while (calculateCost({ itemName, user, quantity: quantity + 1 }) <= currentCoins) { + quantity++ + } + return quantity +} + +const buyRoute = async ({ event, say, words, user }) => { const buying = words[1] setHighestCoins(event.user) + if (!buying) { const highestCoins = user.highestEver || user.coins || 1 if (buyableItems.quade.baseCost < highestCoins * 100) { @@ -75,14 +109,13 @@ const buyRoute = async ({ event, say, words }) => { return } - let quantity = 1 - const currentCoins = getCoins(event.user) + let quantity + const currentCoins = user.coins + const max = maxQuantity({ itemName: buying, user, currentCoins }) if (words[2] === 'max') { - while (calculateCost({ itemName: buying, user, quantity: quantity + 1 }) <= currentCoins) { - quantity++ - } + quantity = max } else { - quantity = parseInt(words[2] || '1') + quantity = Math.round(chaosFilter(parseInt(words[2] || '1'), 0.2, user, max) || 1) } if (!quantity || quantity < 1) { await say('Quantity must be a positive integer') @@ -97,15 +130,17 @@ const buyRoute = async ({ event, say, words }) => { user.coins -= realCost user.items[buying] = user.items[buying] || 0 user.items[buying] += quantity - console.log(buying, user.items.mouse) + if (buying === 'mouse' && user.items.mouse >= 100) { addAchievement(user, 'ratGod', say) } + if (quantity === 1) { await say(`You bought one :${buyable.emoji}:`) } else { await say(`You bought ${quantity} :${buyable.emoji}:`) } + saveGame() } @@ -114,11 +149,11 @@ const buyButton = async ({ body, ack, say, payload }) => { const buying = payload.action_id.substring(4) console.log(`buyButton ${buying} clicked`) const event = { - user: body.user.id, + user: body.user.id } const user = getUser(event.user) - const words = ['', buying, '1'] - await buyRoute({ event, say, words }) + const words = ['', buying, body.actions[0].text] + await buyRoute({ event, say, words, user }) const highestCoins = user.highestEver || user.coins || 1 await slack.app.client.chat.update({ channel: body.channel.id, @@ -129,4 +164,4 @@ const buyButton = async ({ body, ack, say, payload }) => { Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton)) -module.exports = buyRoute \ No newline at end of file +module.exports = buyRoute diff --git a/src/games/hvacoins/buyableItems.js b/src/games/hvacoins/buyableItems.js index acf59eb..ab626f8 100644 --- a/src/games/hvacoins/buyableItems.js +++ b/src/games/hvacoins/buyableItems.js @@ -71,4 +71,16 @@ module.exports = { emoji: 'question', description: 'The elusive creator of Hvacker takes a favorable look at your CPS.' }, + smallBusiness: { + baseCost: 2_210_000_000_000_000, + earning: 2_845_000_000, + emoji: 'convenience_store', + description: 'The place where the creator of Hvacker goes to work.' + }, + bigBusiness: { + baseCost: 26_210_000_000_000_000, + earning: 23_650_000_000, + emoji: 'office', + description: 'The place where the smallBusiness goes to work.' + } } diff --git a/src/games/hvacoins/index.js b/src/games/hvacoins/index.js index d725433..4d6a5cc 100644 --- a/src/games/hvacoins/index.js +++ b/src/games/hvacoins/index.js @@ -5,13 +5,17 @@ const { squadUpgrades, getItemCps, squadIsMissing, - maybeNews, idFromWord, getCoins, getUser, commas, addAchievement, - game: {nfts, squad, users} + shufflePercent, + parseAll, + getRandomFromArray, + chaosFilter, + addReactions, + game: { nfts, squad, users, horrors }, setHighestCoins, definitelyShuffle } = require('./utils') const slack = require('../../slack') const buyableItems = require('./buyableItems') @@ -19,6 +23,9 @@ const upgrades = require('./upgrades') const achievements = require('./achievements') const webapi = require('./webapi') const prestige = require('./prestige') +const lore = require('./lore') +const { getChaos } = require('./quackstore') +const settings = require('./settings') // const readline = require('readline').createInterface({ // input: process.stdin, @@ -41,10 +48,10 @@ const upgradeText = (user, showOwned = false) => { const format = ([key, value]) => `:${getUpgradeEmoji(value)}: *${key}* - ${commas(value.cost)}\n_${value.description}_` return '\n\n' + Object.entries(upgrades) - .filter(userDoesNotHave) - .filter(userMeetsCondition) - .map(format) - .join('\n\n') + + .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' } @@ -55,8 +62,8 @@ const alwaysAccessible = () => true const adminOnly = { hidden: true, condition: ({ event, say }) => { - if (!event.user.startsWith(slack.sageUserId)) { - say(`This is an admin-only command!`) + if (!event.user.startsWith(slack.users.Sage)) { + say('This is an admin-only command!') return false } return true @@ -68,22 +75,33 @@ const testOnly = { } const dmsOnly = { hidden: false, - condition: ({ event, say, words }) => { - if (event.channel_type !== 'im') { - say(`Please use ${words[0]} in DMs only!`) + condition: async ({ event, say, words }) => { + if (!event.channel_type.includes('im')) { + await say(`Please use ${words[0]} in DMs only!`) return false } return true } } -const prestigeOnly = adminOnly // TODO ({ event }) => !!getUser(event.user).prestige +const prestigeOnly = { + hidden: false, + condition: async ({ event, say, words, user }) => { + if (user.prestige) { + await say('Sorry, you must have prestiged to access this menu.') + } + return user.prestige && await dmsOnly.condition({ event, say, words, user }) + } +} 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 } const command = (commandNames, helpText, action, { hidden, condition } = defaultAccess) => { + console.log(`Initializing command '${commandNames[0]}'`) if (!hidden) { commandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')} - ${helpText}\n` + shortCommandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')}` } if (!condition) { condition = alwaysAccessible @@ -108,27 +126,276 @@ const command = (commandNames, helpText, action, { hidden, condition } = default }) } -const messageHandler = async ({ event, say }) => { +const dayOfYear = () => { + const date = new Date() + return ((Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000) +} + +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, words }) => { + const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or about 1 in ${Math.round(1 / odds)}` + if (!words[1]) { + 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(words[1], 99) + 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, words }) => { + const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or 1 in ${Math.round(1 / odds)}` + const num = parseAll(words[1], 99) + const [, , ...message] = words + 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.Sage) { + return slack.postToTechThermostatChannel(shufflePercent(event.text.substring(7).trim(), getShuffleOdds())) + } + horrors.commandCalls ??= 0 + horrors.commandCalls += 1 + await slack.messageSage(`<@${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, words, c }) => async message => { + const punishmentOffset = event.user === slack.users.Quade ? 0 : 0 + const shuffleOdds = getShuffleOdds(punishmentOffset + 99) + + if (typeof message === 'string' && words[0] !== '!n' && words[0] !== '!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.messageSage(`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, + } + } + if (typeof(msg) === 'string') { + return say(slack.encodeData('commandPayload', payload) + msg) + } + return say({ + ...msg, + text: slack.encodeData('commandPayload', payload) + msg.text + }) +} + +const messageHandler = async ({ event, say, isRecycle = false }) => { const words = event?.text?.split(/\s+/) || [] const c = commands.get(words[0]) - if (!c && words[0]?.startsWith('!')) { - return slack.messageSage(`${slack.ourUsers[event.user]} tried to use \`${event.text}\`, if you wanted to add that.`) + if (!c && words[0]?.startsWith('!') && event.user !== slack.users.Sage) { + return slack.messageSage(`${slack.users[event.user]} tried to use \`${event.text}\`, if you wanted to add that.`) } - const canUse = await c?.condition({ event, say, words }) + + const trueSay = say + + say = buildSayWithPayload({ say: trueSay, event }) + if (settings.horrorEnabled) { + say = buildHorrorSay({say, event, words, c}) + } + + const user = getUser(event.user) + user.coins = getCoins(event.user) + const canUse = await c?.condition({ event, say, words, user, userId: event.user, isAdmin: event.user.includes(slack.users.Sage) }) if (!canUse) { // await say(`Command '${words[0]}' not found`) return } + user.interactions = user.interactions || 0 + user.interactions += 1 + if (user.interactions > 999) { + addAchievement(user, 'itsOverNineHundred', say) + } + + const hour = new Date().getHours() + if (hour < 8 || hour > 18) { + addAchievement(user, 'hvackerAfterDark', say) + } + if (words[1] === 'help') { - await say(c.commandNames.map(name => '`' + name + '`').join(', ') + ': ' + c.helpText) + await say(c.commandNames.map(name => `\`${name}\``).join(', ') + ': ' + c.helpText) if (c.commandNames.includes('!coin')) { - addAchievement(getUser(event.user), 'weAllNeedHelp', say) + addAchievement(user, 'weAllNeedHelp', say) } return } - await c.action({ event, say, words }) + + await c.action({ event, say, trueSay, words, user, userId: event.user }) + if (!isRecycle) { + setTimeout(() => lightning({ event, say, trueSay, words, user }), 10000) + } } +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(e)} + } +}) + +const strikes = {} +const lightning = async ({ event }) => { + if (Math.random() > 0.01) { + return + } + if (event.user !== slack.users.Sage) { + await slack.messageSage(`Sent a zap to ${slack.users[event.user]}.`) + } + const message = await slack.app.client.chat.postMessage({ + channel: event.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] = true + setTimeout(async () => { + if (!strikes[message.ts]) { + return + } + delete strikes[message.ts] + await slack.app.client.chat.update({ + channel: event.channel, + ts: message.ts, + text: 'Lightning struck, but you missed it!', + blocks: [] + }) + await slack.messageSage(`They didn't bottle it!`) + }, 5000) +} + +slack.app.action('lightningStrike', async ({ body, ack }) => { + if (!strikes[body.message.ts]) { + await ack() + return + } + delete strikes[body.message.ts] + const c = getCoins(body.user.id) + const secondsOfCps = seconds => Math.floor(getCPS(getUser(body.user.id)) * seconds) + const diff = 500 + Math.floor(c * 0.10) + secondsOfCps(60 * 30) + const user = getUser(body.user.id) + user.coins += diff + saveGame() + + await slack.app.client.chat.update({ + channel: body.channel.id, + ts: body.message.ts, + text: `Lightning successfully bottled! :sake::zap: You got ${commas(diff)} HVAC!`, + blocks: [] + }) + await ack() + await slack.messageSage(`They bottled it!`) +}) + slack.onMessage(async msg => { try { await messageHandler(msg) @@ -137,6 +404,39 @@ slack.onMessage(async msg => { } }) +const chadSpeak = +` ⡜ +⠘⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀ ⠀ ⣀⠴⠊ +⠑⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀ ⣀⠴⠊⠁ +⠘⡀ ⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⣀⠴⠊ +⠈⠢⢄ ⣀⣀⡀⠤⠄⠒⠈ +⠘⣀⠄⠊⠁ +⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠛⠛⠋⠉⠈⠉⠉⠉⠉⠛⠻⢿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⡿⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⢿⣿⣿⣿⣿ +⣿⣿⣿⣿⡏⣀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿ +⣿⣿⣿⢏⣴⣿⣷⠀⠀⠀⠀⠀⢾⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿ +⣿⣿⣟⣾⣿⡟⠁⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣷⢢⠀⠀⠀⠀⠀⠀⠀⢸⣿ +⣿⣿⣿⣿⣟⠀⡴⠄⠀⠀⠀⠀⠀⠀⠙⠻⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⣿ +⣿⣿⣿⠟⠻⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠶⢴⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⣿ +⣿⣁⡀⠀⠀⢰⢠⣦⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⡄⠀⣴⣶⣿⡄⣿ +⣿⡋⠀⠀⠀⠎⢸⣿⡆⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⠗⢘⣿⣟⠛⠿⣼ +⣿⣿⠋⢀⡌⢰⣿⡿⢿⡀⠀⠀⠀⠀⠀⠙⠿⣿⣿⣿⣿⣿⡇⠀⢸⣿⣿⣧⢀⣼ +⣿⣿⣷⢻⠄⠘⠛⠋⠛⠃⠀⠀⠀⠀⠀⢿⣧⠈⠉⠙⠛⠋⠀⠀⠀⣿⣿⣿⣿⣿ +⣿⣿⣧⠀⠈⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠟⠀⠀⠀⠀⢀⢃⠀⠀⢸⣿⣿⣿⣿ +⣿⣿⡿⠀⠴⢗⣠⣤⣴⡶⠶⠖⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡸⠀⣿⣿⣿⣿ +⣿⣿⣿⡀⢠⣾⣿⠏⠀⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠉⠀⣿⣿⣿⣿ +⣿⣿⣿⣧⠈⢹⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿ +⣿⣿⣿⣿⡄⠈⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣦⣄⣀⣀⣀⣀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠙⣿⣿⡟⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⠁⠀⠀⠹⣿⠃⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⢐⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⠿⠛⠉⠉⠁⠀⢻⣿⡇⠀⠀⠀⠀⠀⠀⢀⠈⣿⣿⡿⠉⠛⠛⠛⠉⠉ +⡿⠋⠁⠀⠀⢀⣀⣠⡴⣸⣿⣇⡄⠀⠀⠀⠀⢀⡿⠄⠙⠛⠀⣀⣠⣤⣤⠄` + command( ['!cleanusers'], 'Calls getUser() on all users, ensuring a valid state.', @@ -145,22 +445,181 @@ command( 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 addReaction = async emojiName => + // slack.app.client.reactions.add({ + // channel: event.channel, + // timestamp: sent.ts, + // name: emojiName + // }) + // await addReaction('one') + // await addReaction('two') + // await addReaction('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 ({ event, say, words }) => { - const user = getUser(event.user) + async ({ say, words, user }) => { + if (words[2]) { + return say(`Your password may not contain spaces!`) + } user.pwHash = webapi.makeHash(words[1]) await 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, words, event, user }) => { + if (words[1] !== 'hvacker' && words[1] !== `<@${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 }) => say('```' + commandHelpText + '```') + async ({ say, words, user }) => { + if (settings.horrorEnabled && (words[1] === 'hvacker' || words[1] === `<@${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 (words[1] === 'full') { + return say('```' + commandHelpText + '```') + } + return say(shortCommandHelpText + '```') + } ) const removeAchievement = async (user, name, say) => { @@ -184,19 +643,19 @@ command( command( ['!a', '!ach', '!achievements'], 'List your glorious achievements', - async ({ event, say }) => { - const user = getUser(event.user) - + async ({ event, say, user }) => { const achievementCount = Object.keys(user.achievements).length const prefix = `You have ${achievementCount} achievements!\n\n` const mult = (Math.pow(1.01, achievementCount) - 1) * 100 - let list = '' - if (event.channel_type === 'im') { - list = Object.keys(user.achievements) + 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(({ description, emoji, name }) => `:${emoji}: *${name}* - ${description}`) - .join('\n') + '\n\n' - } + .map(desc) + .join(isIm ? '\n' : ' ') + '\n\n' const postfix = achievementCount ? `_Achievements are boosting your CPS by ${mult.toPrecision(3)}%_` : '' await say(prefix + list + postfix) @@ -211,82 +670,139 @@ const emojiLine2 = (itemName, countOwned) => const emojiLine3 = (itemName, countOwned) => (countOwned < 5 ? s => s.repeat(countOwned) - : s => s + ` x${countOwned}`) - (buyableItems[itemName].emoji) + : s => s + ` x${countOwned}`)(buyableItems[itemName].emoji) const emojiLine = (itemName, countOwned) => countOwned < 5 ? `:${buyableItems[itemName].emoji}:`.repeat(countOwned) : `:${buyableItems[itemName].emoji}: x${countOwned}` -const collection = userId => - Object.entries(users[userId]?.items || {}) - .map(([itemName, countOwned]) => emojiLine(itemName, countOwned) + ' - ' + commas(Math.round(getItemCps(getUser(userId), itemName))) + ' cps') +const collection = user => + Object.entries(user?.items || {}) + .map(([itemName, countOwned]) => emojiLine(itemName, countOwned) + ' - ' + commas(getItemCps(user, itemName)) + ' cps') .join('\n') command(['!cps'], - 'Display your current Coins Per Second', - async ({ event, say }) => - say(`You are currently earning \`${commas(getCPS(event.user))}\` HVAC Coin per second.`)) + '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 => Math.floor(getCPS(user) * seconds) + let diff + let prefix + if (random > 0.9967) { + diff = 500 + Math.floor(c * 0.10) + secondsOfCps(60 * 30) + prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n` + addAchievement(user, 'luckyGem', say) + await slack.messageSage(`${slack.users[userId]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`) + } else if (random > 0.986) { + diff = 50 + Math.floor(c * 0.025) + secondsOfCps(60) + prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n` + addAchievement(user, 'goldBrick', say) + } else if (random > 0.96) { + diff = 10 + Math.floor(c * 0.01) + secondsOfCps(10) + prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n` + addAchievement(user, 'greenCoin', say) + } else { + prefix = 'You mined one HVAC.\n' + diff = 1 + } + user.coins = c + diff + saveGame() + return `${prefix}You now have ${commas(user.coins)} HVAC coin${c !== 1 ? 's' : ''}. Spend wisely.` +} command( ['!c', '!coin', '!mine', '!'], 'Mine HVAC coins', - async ({ event, say }) => { - const user = getUser(event.user) - maybeNews(say) - const random = Math.random() - const c = getCoins(event.user) - const secondsOfCps = seconds => Math.floor(getCPS(event.user) * seconds) - const hour = new Date().getHours() - if (hour < 8 || hour > 18) { - addAchievement(user, 'hvackerAfterDark', say) - } - let diff - let prefix - if (random > 0.9967) { - diff = 500 + Math.floor(c * 0.10) + secondsOfCps(60 * 30) - prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n` - addAchievement(user, 'luckyGem', say) - await slack.messageSage(`${slack.ourUsers[event.user]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`) - } else if (random > 0.986) { - diff = 50 + Math.floor(c * 0.025) + secondsOfCps(60) - prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n` - addAchievement(user, 'goldBrick', say) - } else if (random > 0.96) { - diff = 10 + Math.floor(c * 0.01) + secondsOfCps(10) - prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n` - addAchievement(user, 'greenCoin', say) - } else { - prefix = 'You mined one HVAC.\n' - diff = 1 - } - user.coins += diff - await say(`${prefix}You now have ${commas(user.coins)} HVAC coin` + (c !== 1 ? 's' : '') + '. Spend wisely.') - saveGame() + async ({ say, user, userId }) => { + await say(await doMine({ user, userId, say })) } ) +command( + ['!as'], + 'Run commands as another user.', + async ({ event, words, trueSay }) => { + const [bangAs, impersonating, ...newWords] = words + event.user = idFromWord(impersonating) + event.text = newWords.join(' ') + await messageHandler({ event, say: trueSay, isRecycle: false}) + }, adminOnly) + command( ['!g', '!gamble'], 'Gamble away your HVAC\n' + ' To use, say \'gamble coin_amount\' or \'!gamble all\'', - async ({ event, say, words }) => { - getCoins(event.user) // Update coin counts - const user = getUser(event.user) + async ({ event, say, words, user }) => { - let n - if (words[1].toLowerCase() === 'all') { - if (user.coins === 0) { - return say('You don\'t have any coins!') - } - n = user.coins - } else { - n = parseInt(words[1]) - } + const [, ...wager] = words + const requestedWager = parseAll(wager.join(' '), user.coins) + const n = (chaosFilter(requestedWager, 0.2, user, user.coins) + requestedWager) / 2 if (!n || n < 0) { return say(`Invalid number '${n}'`) } if (user.coins < n) { - return say(`You don\'t have that many coins! You have ${commas(user.coins)}.`) + return say(`You don't have that many coins! You have ${commas(user.coins)}.`) } if (n >= 100_000_000_000) { addAchievement(user, 'bigBets', say) @@ -300,8 +816,18 @@ command( outcome = 'lost' } console.log(`They ${outcome}`) - await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`) saveGame() + await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`) + if (outcome === 'lost') { + switch (event.user) { + case slack.users.Quade: + await say(':trimedary_anim:') + break; + case slack.users.Houston: + await say(':fortnite:') + break; + } + } } ) @@ -309,7 +835,7 @@ command( ['!buynft', '!bn'], 'Acquire high-quality art\n' + ' To use, say \'!buynft nft_name\'', - async ({ event, say, words }) => { + async ({ event, say, words, user }) => { const nft = nfts.find(n => n.name.toLowerCase() === words[1]) if (!nft) { const suffix = words[1]?.match(/[^a-z0-9_]/i) ? '. And I\'m unhackable, so cut it out.' : '' @@ -318,12 +844,12 @@ command( if (nft.owner) { return say('Someone already owns that NFT!') } - const c = getCoins(event.user) + const c = user.coins if (c < nft.price) { return say('You don\'t have enough coin for this nft') } - users[event.user].coins -= nft.price + user.coins -= nft.price nft.owner = event.user saveGame() await say('You bought ' + nft.name + '!') @@ -333,8 +859,7 @@ command( command( ['!myupgrades', '!myu'], 'List all the upgrades that you own.', - async ({ event, say }) => { - const user = getUser(event.user) + async ({ say, user }) => { await say(upgradeText(user, true)) }, dmsOnly) @@ -342,8 +867,7 @@ command( ['!upgrade', '!u'], 'Improve the performance of your HVAC-generators.\n' + ' Say \'!upgrade\' to list available upgrades, or \'!upgrade upgrade_name\' to purchase.', - async ({ event, say, words }) => { - const user = getUser(event.user) + async ({ say, words, user }) => { const upgradeName = words[1] if (!upgradeName) { return say(upgradeText(user)) @@ -361,7 +885,7 @@ command( if (!upgrade.condition(user)) { return say('That item does not exist!') } - const c = getCoins(event.user) + const c = user.coins if (c < upgrade.cost) { return say(`You don't have enough coins! You have ${commas(c)}, but you need ${commas(upgrade.cost)}`) } @@ -408,7 +932,7 @@ command( '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 ({ event, say, words }) => { + async ({ say, words, user }) => { if (!words[1]) { return say(squadText()) } @@ -416,15 +940,10 @@ command( if (!current) { return say('No squadgrades are currently available') } - const currentCoins = getCoins(event.user) - let amount - if (words[1] === 'all') { - amount = currentCoins - } else { - amount = parseInt(words[1]) - if (amount > currentCoins) { - return say(`You don't have that much HVAC! You have ${currentCoins}.`) - } + const currentCoins = user.coins + let amount = parseAll(words[1], currentCoins) + if (amount > currentCoins) { + return say(`You don't have that much HVAC! You have ${currentCoins}.`) } if (!amount || amount < 1) { return say(`Invalid amount: '${words[1]}'`) @@ -432,7 +951,6 @@ command( if (amount > squad.upgrades[current.name]) { amount = squad.upgrades[current.name] } - const user = getUser(event.user) squad.upgrades[current.name] -= amount user.coins -= amount if (squad.upgrades[current.name] <= 0) { @@ -445,11 +963,11 @@ command( const buyRoute = require('./buy') command( - ['!buy', '!b'], - 'Buy new items to earn HVAC with\n' + + ['!buy', '!b'], + '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.', - buyRoute + buyRoute ) command( @@ -461,14 +979,13 @@ command( return say('Target must be a valid @') } - if (targetId !== 'U0344TFA7HQ') { - const coins = getCoins(targetId) - await say(`<@${targetId}> has ${commas(coins)} HVAC.`) - } else { + 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) - await say(`Hvacker owns ${humanMembers.length} souls.`) + return say(`Hvacker owns ${humanMembers.length} souls.`) } + const coins = getCoins(targetId) + await say(`<@${targetId}> has ${commas(coins, words[2] === 'exact')} HVAC.`) } ) @@ -476,11 +993,9 @@ command( ['!gift', '!give', '!gi'], 'Donate coins to a fellow player\n' + ' Send coins by saying \'!gift @player coin_amount\'', - async ({ event, words, say }) => { - const userId = event.user - const user = getUser(userId) + async ({ words, say, user }) => { const [, target, amountText] = words - const amount = amountText === 'all' ? getCoins(userId) : parseInt(amountText) + const amount = parseAll(amountText, user.coins) const targetId = idFromWord(target) if (!amount || amount < 0) { return say('Amount must be a positive integer!') @@ -491,7 +1006,7 @@ command( if (user.coins < amount) { return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`) } - if (amountText === 'all' && slack.ourUsers[targetId] === 'Tyler') { + if (amountText === 'all' && slack.users.Tyler === targetId) { addAchievement(user, 'walmartGiftCard', say) } const targetUser = getUser(targetId) @@ -501,14 +1016,23 @@ command( } ) +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 }) => { + async ({ event, say, user }) => { await say( - `You are currently earning \`${commas(getCPS(event.user))}\` HVAC Coin per second.\n\n` + - `You currently have ${commas(getCoins(event.user))} HVAC Coins\n\n` + - `${collection(event.user)}\n\nCoins collected all-time: ${commas(Math.round(users[event.user].coinsAllTime))}\n\n` + `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` ) } ) @@ -516,12 +1040,11 @@ command( command( ['!gimme'], 'Give self x coins', - async ({ event, say, words }) => { - const user = getUser(event.user) + async ({ say, words, user }) => { const increase = parseInt(words[1].replace(/,/g, '')) user.coins += increase user.coinsAllTime += increase - await say(`You now have ${getCoins(event.user)} HVAC.`) + await say(`You now have ${user.coins} HVAC.`) }, testOnly) command( @@ -529,7 +1052,7 @@ command( '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, words }) => { - const owner = nft => `Owner: *${slack.ourUsers[nft.owner] || 'NONE'}*` + 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 filter = words[1] ? nft => words[1]?.toLowerCase() === nft.name : null @@ -542,37 +1065,79 @@ command( } ) +const prestigeEmoji = user => { + const p = user.prestige || 0 + if (!p) { + return '' + } + if (p < 10) { + return ':star:' + } + if (p < 20) { + return ':star2:' + } + if (p < 30) { + return ':stars:' + } +} + command( ['!leaderboard', '!lb'], - 'Show the top HVAC-earners, ranked by CPS', - async ({ event, say }) => { - const user = getUser(event.user) - await say('```' + + 'Show the top HVAC-earners, ranked by prestige, then CPS', + async ({ say, user }) => { + // if ((event.user === slack.users.Houston || event.user === slack.users.Sage) && event.channel_type.includes('im')) { + // return say('```' + `Hvacker - 9 souls - Taking from them whatever it desires\nSome other losers - who cares - whatever` + '```') + // } + let index = 1 + await say( Object.entries(users) - .filter(([id]) => getCPS(id) !== 0) - .sort(([id], [id2]) => getCPS(id) > getCPS(id2) ? -1 : 1) - .map(([id]) => `${slack.ourUsers[id] || '???'} - ${commas(getCPS(id))} CPS - ${commas(getCoins(id))} HVAC`) - .join('\n') + '```' + .filter(([id, user]) => Object.entries(user.items).length > 0) + .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]) => `${index++}. ${slack.users[id] || '???'} ${prestigeEmoji(u) || '-'} ${commas(getCPS(getUser(id)))} CPS - ${commas(getCoins(id))} HVAC`) + .join('\n') ).then(() => addAchievement(user, 'leaderBoardViewer', say)) } ) -const oneShot = (name, helpText, message, achievementName) => - command([name], helpText, async ({ event, say }) => { - await say(message) - await slack.messageSage(`Wow buddy they like your ${name} joke.`) - if (achievementName) { - addAchievement(getUser(event.user), achievementName, say) - } - }, { hidden: true }) +const hints = [] +const oneShot = (name, helpText, message, achievementName) => { + if (helpText) { + hints.push(helpText) + } + + command([name], helpText, async ({ say, user }) => { + await say(message) + await slack.messageSage(`Wow buddy they like your ${name} joke.`) + if (achievementName) { + addAchievement(user, achievementName, say) + } + }, { hidden: true }) +} oneShot('!santa', 'Ho ho ho!', '') -oneShot('!sugma', 'GRRRR', ':hvacker_angery:') -oneShot('!pog', 'Displays a poggers hvacker', '') -oneShot('!ligma', 'Toy with me not, son.', '') +oneShot('!sugma', 'Not very original.', ':hvacker_angery:') +oneShot('!pog', 'One poggers hvacker', '') +oneShot('!ligma', 'Not very original.', '') oneShot('!dab', 'ACTIVATE COOL GUY MODE', '', 'certifiedCoolGuy') oneShot('!based', 'Sorry, it\'s a little hard to hear you!', '') -oneShot('!shrek', 'Love and life, baby.', '') +oneShot('!shrek', 'Is love and is life.', '') +// 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'], @@ -597,24 +1162,38 @@ command( }, adminOnly) command( - ['!prestige', '!p'], - 'Show your current prestige status', - prestige.prestigeRoute, - adminOnly + ['!prestige', '!p'], + 'Show your current prestige status', + prestige.prestigeRoute ) command( - ['!!prestige'], - 'Confirm your prestige activation.', - prestige.prestigeConfirmRoute, - adminOnly + ['!!prestige'], + 'Confirm your prestige activation.', + prestige.prestigeConfirmRoute ) command( - ['!quack', '!quackstore'], - 'Spend your prestigious quackings', - prestige.quackStoreRoute, - prestigeOnly + ['!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( @@ -623,54 +1202,76 @@ command( async ({ words, say }) => { const target = words[1] const targetId = idFromWord(target) + const targetUser = getUser(targetId) await say( - `${target} is currently earning \`${commas(getCPS(targetId))}\` HVAC Coin per second.\n\n` + + `${target} is currently earning \`${commas(getCPS(targetUser))}\` HVAC Coin per second.\n\n` + `They have ${commas(getCoins(targetId))} HVAC Coins\n\n` + - `${collection(targetId)}\n\n` + - `${Object.entries(users[targetId]?.items || {}).reduce((total, [name, count]) => total + count, 0)} total items\n\n` + `${collection(targetUser)}\n\n` + + `${Object.entries(targetUser?.items || {}).reduce((total, [name, count]) => total + count, 0)} total items\n\n` ) }, adminOnly) -const exec = require('child_process').exec; +const exec = require('child_process').exec +const fs = require('fs') command( ['!pl'], - '!pl `code`', - async ({ event, say}) => { - const code = event.text.substring(4) + '!pl `code`\n\n' + + 'A very very stupid lisp implementation.', + async ({ event, say }) => { + let code = ' '// = '(def iLoveHvacker "9jklFUlbnd38bCrrU9765FhN") ' + code += event.text.substring(4) .replace(/`/g, '') .replace(/</g, '<') - console.log('PL CODE:', code) - const child = exec(`/home/sagevaillancourt/projects/pebblisp/src/pl '${code}'`) - let result = '```' + 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 ${fileName}` + console.log(command) + const child = exec(command) + let result = '```\n' let errors = '' - child.stdout.on('data', data => result += data) + 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 () => { - if (result.length > 2000) { - result = result.substring(0, 2000) + '...\nOUTPUT EXCEEDS MAXIMUM AND WAS TRUNCATED\n' + - 'STDERR:\n' + errors.substring(0, 200) + // 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` } - await say(result + '```') + 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) + }, { hidden: true, condition: ({ event }) => event.user.startsWith(slack.users.Sage) }) command( ['!steal', '!sagesteal'], 'Highly illegal', - async ({ event, words, say }) => { - const userId = event.user - const user = getUser(userId) + async ({ event, words, say, user }) => { const [, target] = words - const amount = getCoins(userId) / 100 + 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.sageUserId + targetId = slack.users.Sage } if (user.coins < amount) { return @@ -678,20 +1279,28 @@ command( const targetUser = getUser(targetId) user.coins -= amount targetUser.coins += amount - await say(`Stealing is wrong. Gave ${commas(Math.floor(amount))} of your HVAC to ${slack.ourUsers[targetId]}`) + await say(`Stealing is wrong. Gave ${commas(amount)} of your HVAC to ${slack.users[targetId]}`) }, { hidden: true }) // TODO command( - ['!lottery'], + ['!lotto'], 'Once per day, try for a big win!', - async ({ event, say }) => { - const user = getUser(event.user) + async ({ event, say, user }) => { const currentDate = new Date().getDate() - const lastLotto = user.lastLotto || currentDate - 1 - if (lastLotto === currentDate) { + const lastLotto = user.lastLotto === undefined ? currentDate - 1 : user.lastLotto + if (lastLotto === currentDate && event.user !== slack.users.Sage) { return say('Hey, only one lotto ticket per day, alright?') } + let msg + if (Math.random() < 0.01) { + const prize = user.coinsAllTime / 20 + msg = `Ayyyyy, we have a winner! You win ${commas(prize)} HVAC!` + user.coins += prize + } else { + msg = `Sorry pal, today's not your lucky day.` + } + await say(msg) user.lastLotto = currentDate saveGame() }, { hidden: true }) @@ -699,18 +1308,14 @@ command( command( ['!ignite'], 'You found me!', - async ({ event, say }) => { - const user = getUser(event.user) + async ({ say, user }) => { addAchievement(user, 'ignited', say) }, { hidden: true }) command( ['!giveach'], '!giveach @player ach_name', - async ({ words, say }) => { - const targetId = idFromWord(words[1]) - const user = getUser(targetId) - console.log(user, words[2], say) + async ({ words, say, user }) => { addAchievement(user, words[2], say) saveGame() }, adminOnly) @@ -727,14 +1332,14 @@ command( async ({ event, words, say }) => { const targetId = idFromWord(words[1]) if (!targetId) { - return say(`Please specify a valid @ target!`) + return say('Please specify a valid @ target!') } const nft = nfts.find(nft => nft.name === words[2]) if (!nft) { return say(`There is not NFT named "${words[2]}"!`) } if (nft.owner !== event.user) { - return say(`You do not own "${nft.name}"!`) + return say(`You do not own "${nft.name}"!`) } nft.owner = targetId }, { hidden: true }) @@ -753,11 +1358,10 @@ command( async ({ event, words, say }) => { const targetId = idFromWord(words[1]) if (!targetId) { - return say(`Please specify a valid @ target!`) + return say('Please specify a valid @ target!') } const msg = event.text.replace(/\S+ +\S+\s*/i, '') await slack.messageIn(targetId, msg) - await slack.messageSage(msg) }, adminOnly) -webapi.launch() \ No newline at end of file +// webapi.launch() diff --git a/src/games/hvacoins/lore.js b/src/games/hvacoins/lore.js new file mode 100644 index 0000000..c41b74c --- /dev/null +++ b/src/games/hvacoins/lore.js @@ -0,0 +1,113 @@ +const { setHighestCoins, addAchievement, chaosFilter, commas, saveGame, getUser } = require('./utils') +const slack = require('../../slack') + +let loreCount = 0 +const l = (text, {correctReactions, correctResponse, incorrectResponse, jumpTo} = {}) => { + loreCount += 1 + return { + text, + correctReactions, + correctResponse, + incorrectResponse, + jumpTo + } +} + +const lore = [ + l(`Allow me to tell you a story.`), + l(`Once upon a time, there were two young ducks named Genevieve and Isaiah.`), + l(`Isaiah was known mostly for being a big butthole whomst no one liked.`), + l(`Genevieve was known for a singing voice that could peel the paint off your car.`), + l(`Nevertheless, there's was a passionate love affair.`), + l(`_Honking_`), + l(`...`), + l(`Hey you know, it's rather cold, don't you think? Could you start a fire for us?`, { + correctReactions: ['fire'], + correctResponse: `Thank you.`, + incorrectResponse: `Well, you're looking in the right place, anyway.`, + }), + + l(`Anyway, together they laid nine eggs.`), + l(`The first to hatch was Rick, who grew up into a fine bird with beautiful feathers.`), + l(`The second was Claire, who grew into an even finer bird, glowing with Duckish beauty.`), + l(`The third was Marf, who developed a severe addiction to Elmer's glue.`), + l(`Bird four was Yoink, who considered Tupperware a hobby.`), + l(`The fifth was Big Bob.`), + l(`The sixth was Jess, who became the number one checkers player in the world.`), + l(`The seventh was Small Bob.`), + l(`The eighth was Maurice, who had quite a few words to say about the French, and was eventually elected president.`), + l(`And the ninth...`), + l(`Well, the ninth might actually amount to something.`), + l(`https://i.imgur.com/eFreg7Y.gif\n`), +] + +slack.onReaction(async ({ event, say }) => { + try { + const user = getUser(event.user) + const item = await slack.getMessage({ channel: event.item.channel, ts: event.item.ts }) + const message = item.messages[0].text + const loreData = slack.decodeData('lore', message) + if (!loreData || user.lore !== loreData.index || !loreData.correctReactions) { + return + } + if (!loreData.correctReactions.includes(event.reaction)) { + if (lore[user.lore].incorrectResponse) { + await say(lore[user.lore].incorrectResponse + encodeLore(user.lore)) + } + return + } + console.log(lore[user.lore]) + await say(lore[user.lore].correctResponse) + user.lore += 1 + saveGame() + } catch (e) {console.error(e)} +}) + +const encodeLore = loreNumber => lore[loreNumber].text.startsWith(':') && lore[loreNumber].text.endsWith(':') ? '' : + slack.encodeData('lore', { + index: loreNumber, + correctReactions: lore[loreNumber].correctReactions + }) + +const loreMessage = (user, say) => { + if (lore[user.lore]) { + return lore[user.lore].text + encodeLore(user.lore) + } + addAchievement(user, 'bookWorm', say) + return `Sorry. I'd love to tell you more, but I'm tired. Please check back later.` +} + +const loreRoute = async ({ say, words, user, isAdmin }) => { + user.lore ??= 0 + if (!words[1]) { + const message = loreMessage(user, say) + await say(message) + if (!lore[user.lore]?.correctReactions) { + user.lore += 1 + } + saveGame() + console.log('Sent ' + user.name + ':\n' + message) + return + } + if (words[1] === 'reset') { + user.lore = 0 + saveGame() + return say(`I have reset your place in the story.`) + } + if (isAdmin) { + if (words[1] === 'all') { + let loreMessage = '' + for (let i = 0; i < user.lore; i++) { + loreMessage += lore[i].text + (lore[i].correctResponse || '') + '\n' + } + return say(loreMessage) + } + const jumpTo = parseInt(words[1]) + if (!isNaN(jumpTo)) { + user.lore = jumpTo + saveGame() + } + } +} + +module.exports = loreRoute diff --git a/src/games/hvacoins/prestige.js b/src/games/hvacoins/prestige.js index 210e509..2656279 100644 --- a/src/games/hvacoins/prestige.js +++ b/src/games/hvacoins/prestige.js @@ -1,111 +1,107 @@ -const { getUser, getCoins, commas, saveGame } = require('./utils'); -const quackStore = require('./quackstore'); +const { commas, saveGame, quackGradeMultiplier, prestigeMultiplier, makeBackup } = require('./utils') +const { quackStore } = require('./quackstore') const possiblePrestige = coins => { let p = 0 - while (tpcRec(p + 1) <= coins) { + while (totalCostForPrestige(p + 1) <= coins) { p += 1 } return p } -const totalCostForPrestige = prestigeLevel => { - let cost = 0 - while (prestigeLevel) { - cost += 1_000_000_000_000 * Math.pow(prestigeLevel, 3) - prestigeLevel -= 1 - } - return cost -} - const tpcRecMemo = [] -const tpcRec = prestigeLevel => { +const totalCostForPrestige = prestigeLevel => { if (prestigeLevel === 0) { return 0 } - return (tpcRecMemo[prestigeLevel]) || (tpcRecMemo[prestigeLevel] = 1_000_000_000_000 * Math.pow(prestigeLevel, 3) + tpcRec(prestigeLevel - 1)) + return (tpcRecMemo[prestigeLevel]) || (tpcRecMemo[prestigeLevel] = 1_000_000_000_000 * Math.pow(prestigeLevel, 3) + totalCostForPrestige(prestigeLevel - 1)) } -// TODO -const prestigeRoute = async ({ event, say, words }) => { - const user = getUser(event.user) - getCoins(event.user) - const possible = possiblePrestige(user.coinsAllTime) - const current = user.prestige ??= 0 - if (words[1] === 'me') { - await say( - 'This will permanently remove all of your items, upgrades, and coins!\n\n' + +const prestigeRoute = async ({ say, words, user }) => { + const possible = possiblePrestige(user.coinsAllTime) + const current = user.prestige ??= 0 + if (words[1] === 'me') { + await say( + 'This will permanently remove all of your items, upgrades, and coins!\n\n' + 'Say \'!!prestige me\' to confirm.' - ) - } else { - await say( - `Current Prestige: ${commas(current)}\n\n` + - `Quacks gained if you prestige now: ${commas(possible - current)}\n\n` + - `HVAC until next quack: ${commas(Math.round(tpcRec(possible + 1) - user.coinsAllTime))}\n\n` + - 'Say \'!prestige me\' to start the prestige process.' - ) - } - }//, true, adminOnly) + ) + } else { + await say( + `Current Prestige: ${commas(current)}\n\n` + + `Quacks gained if you prestige now: ${commas(possible - current)}\n\n` + + `HVAC until next quack: ${commas(totalCostForPrestige(possible + 1) - user.coinsAllTime)}\n\n` + + 'Say \'!prestige me\' to start the prestige process.' + + `\n\nYour prestige is currently boosting your CPS by ${commas((prestigeMultiplier(user) - 1) * 100)}%` + ) + } +}//, true, adminOnly) -// TODO -const prestigeConfirmRoute = async ({ event, say, words }) => { - const user = getUser(event.user) - getCoins(event.user) - const possible = possiblePrestige(user.coinsAllTime) - const current = user.prestige - if (possible <= current) { - await say('You don\'t have enough HVAC to prestige right now!') - return - } - if (event?.text !== '!!prestige me') { - await say('Say exactly \'!!prestige me\' to confirm') - return - } - console.log('possible', possible) - console.log('user.prestige', user.prestige) - user.quacks ??= 0 - user.quacks += (possible - user.prestige) - console.log('user.quacks', user.quacks) - user.prestige = possible - user.coins = 0 - user.items = {} - user.upgrades = {} - saveGame() - await say('You prestiged! Check out !quackstore to see what you can buy!') - } +const prestigeConfirmRoute = async ({ event, say, user }) => { + const possible = possiblePrestige(user.coinsAllTime) + const current = user.prestige + if (possible <= current) { + await say('You don\'t have enough HVAC to prestige right now!') + return + } + if (event?.text !== '!!prestige me') { + await say('Say exactly \'!!prestige me\' to confirm') + return + } + await makeBackup() -const quackStoreListing = ([name, upgrade]) => - `:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_` + user.quacks ??= 0 + user.quacks += (possible - user.prestige) + + user.prestige = possible + user.coins = 0 + user.items = {} + user.upgrades = {} + + saveGame() + await say('You prestiged! Check out !quackstore to see what you can buy!') +} + +const quackStoreListing = (showCost = true) => ([name, upgrade]) => + showCost + ? `:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_` + : `:${upgrade.emoji}: *${name}* - Worth *${upgrade.cost} Quack.*\n\n_${upgrade.description}_` const allUserQuackUpgrades = user => - Object.entries(user.quackUpgrades || {}) - .map(([type, upgrades]) => upgrades) + Object.entries(user.quackUpgrades || {}) + .map(([type, upgrades]) => upgrades).flatMap(x => x) const hasPreReqs = user => ([name, upgrade]) => { if (!upgrade.preReqs) { return true } const allUserUpgrades = allUserQuackUpgrades(user) - console.log(allUserUpgrades) return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq)) } -const quackStoreText = user => - Object.entries(quackStore) - .filter(hasPreReqs(user)) - .map(quackStoreListing) - .join('\n\n') +const owns = (user, [name, upgrade]) => allUserQuackUpgrades(user).includes(name) -const quackStoreRoute = async ({ event, say, words }) => { - const user = getUser(event.user) +const ownedQuackItems = user => Object.entries(quackStore).filter(upgrade => owns(user, upgrade)) + +const unownedQuackItems = user => Object.entries(quackStore).filter(upgrade => !owns(user, upgrade)) + +const quackStoreText = user => + unownedQuackItems(user) + .filter(hasPreReqs(user)) + .map(quackStoreListing(true)) + .join('\n\n') + + `\n\nYou have ${user.quacks ??= 0} quacks to spend.` + + `\nQuackStore upgrades are currently boosting your CPS by ${commas((quackGradeMultiplier(user) - 1) * 100)}%` + +const quackStoreRoute = async ({ user, say, words }) => { user.quackUpgrades ??= {} const quacks = user.quacks ??= 0 if (!words[1]) { await say(quackStoreText(user)) return } + console.log(`Trying to buy ${words[1]}`) const quackItem = quackStore[words[1]] - if (!quackItem) { + if (!quackItem || !unownedQuackItems(user).find(([name]) => name === words[1])) { await say(`'${words[1]}' is not available in the quack store!`) return } @@ -113,13 +109,28 @@ const quackStoreRoute = async ({ event, say, words }) => { await say(`${words[1]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`) return } + user.quacks -= quackItem.cost user.quackUpgrades[quackItem.type] ??= [] user.quackUpgrades[quackItem.type].push(words[1]) saveGame() } +const ownedQuacksText = user => + ownedQuackItems(user) + .filter(hasPreReqs(user)) + .map(quackStoreListing(false)) + .join('\n\n') + + `\n\nQuackStore upgrades are currently boosting your CPS by ${commas((quackGradeMultiplier(user) - 1) * 100)}%` + +const ownedQuacksRoute = async ({ say, user }) => { + user.quackUpgrades ??= {} + user.quacks ??= 0 + await say(ownedQuacksText(user)) +} + module.exports = { quackStoreRoute, prestigeRoute, - prestigeConfirmRoute + prestigeConfirmRoute, + ownedQuacksRoute } diff --git a/src/games/hvacoins/quackstore.js b/src/games/hvacoins/quackstore.js index 2431c37..2a33e9d 100644 --- a/src/games/hvacoins/quackstore.js +++ b/src/games/hvacoins/quackstore.js @@ -1,3 +1,10 @@ +const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)] + +const chaosCpsMods = [3, 2, 0.1, 1, 1.5, 1.6, 0, 1.1, 1.1, 1.26] +const chaosAvg = () => chaosCpsMods.reduce((total, next) => total + next, 0) / chaosCpsMods.length +//const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length) + offset) % chaosCpsMods.length] +const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length)) % chaosCpsMods.length] + const quackStore = { ascent: { name: 'Ascent', @@ -16,5 +23,24 @@ const quackStore = { effect: cps => cps * 1.2, cost: 5 }, + chaos: { + name: 'Chaos', + type: 'cps', + emoji: 'eye', + description: 'Awaken. Gives a random modifier to your CPS every six seconds. May have other consequences...', + //+ '_\n_Averages a 26% CPS boost.', + preReqs: ['nuclearFuel'], + effect: (cps, user) => { + if (user.name !== 'Sage') { + console.log('Chaos Multiplier', getChaos(Math.round(user.interactions / 50))) + } + return cps * getChaos(Math.round(user.interactions / 50)) + }, + cost: 10 + } +} + +module.exports = { + quackStore, + getChaos: user => getChaos(user.interactions || 0) } -module.exports = quackStore \ No newline at end of file diff --git a/src/games/hvacoins/settings.js b/src/games/hvacoins/settings.js new file mode 100644 index 0000000..c2d3207 --- /dev/null +++ b/src/games/hvacoins/settings.js @@ -0,0 +1,3 @@ +module.exports = { + horrorEnabled: false +} \ No newline at end of file diff --git a/src/games/hvacoins/upgrades.js b/src/games/hvacoins/upgrades.js index 3a3e7df..67d6d66 100644 --- a/src/games/hvacoins/upgrades.js +++ b/src/games/hvacoins/upgrades.js @@ -11,191 +11,209 @@ module.exports = { type: 'mouse', description: 'Doubles the power of mice', count: 1, - cost: 1_000, + cost: 1_000 }), stinkierCheese: basic({ type: 'mouse', description: 'Mice are doubly motivated to hunt down HVAC Coins', - count: 10, - cost: 21_000, + count: 10, + cost: 21_000 }), biggerTeeth: basic({ type: 'mouse', description: 'Mice can intimidate twice as much HVAC out of their victims.', - count: 25, - cost: 50_000, + count: 25, + cost: 50_000 }), fasterComputers: basic({ type: 'accountant', description: 'Accountants can ~steal~ optimize twice as much HVAC!', count: 1, - cost: 11_000, + cost: 11_000 }), lackOfMorality: basic({ type: 'accountant', description: 'Accountants are taking a hint from nearby CEOs.', - count: 10, - cost: 200_000, + count: 10, + cost: 200_000 }), widerBrains: basic({ type: 'accountant', description: 'For accountant do double of thinking.', - count: 25, - cost: 550_000, + count: 25, + cost: 550_000 }), biggerBlowhole: basic({ type: 'whale', description: 'With all that extra air, whales have double power!', count: 1, - cost: 120_000, + cost: 120_000 }), sassyWhales: basic({ type: 'whale', description: 'These are the kind of whales that know how to get twice as much done', - count: 10, - cost: 3_000_000, + count: 10, + cost: 3_000_000 }), thinnerWater: basic({ type: 'whale', description: 'Whales can move twice as quickly through this physics-defying liquid', - count: 25, - cost: 6_000_000, + count: 25, + cost: 6_000_000 }), greasyTracks: basic({ type: 'train', description: 'Lets trains deliver HVAC twice as efficiently', count: 1, - cost: 1_300_000, + cost: 1_300_000 }), rocketThrusters: basic({ type: 'train', description: 'That\'ll put some quack on your track', - count: 10, - cost: 22_000_000, + count: 10, + cost: 22_000_000 }), loudConductors: basic({ type: 'train', description: 'Conductors can onboard twice as much HVAC', - count: 25, - cost: 65_000_000, + count: 25, + cost: 65_000_000 }), gasolineFire: basic({ type: 'fire', description: 'Extremely good for breathing in.', count: 1, - cost: 14_000_000, + cost: 14_000_000 }), extremelyDryFuel: basic({ type: 'fire', description: 'Use the ignite command for a secret achievement.', count: 10, - cost: 163_000_000, + cost: 163_000_000 }), cavemanFire: basic({ type: 'fire', description: 'They just don\'t make \'em like they used to.', - count: 25, - cost: 700_000_000, + count: 25, + cost: 700_000_000 }), spoonerang: basic({ type: 'boomerang', description: 'Scoops up HVAC mid-flight', count: 1, - cost: 200_000_000, + cost: 200_000_000 }), boomerAng: basic({ type: 'boomerang', description: 'It\'s... old.', count: 10, - cost: 1_200_000_000, + cost: 1_200_000_000 }), doubleRang: basic({ type: 'boomerang', description: 'You throw one, but somehow catch two', - count: 25, - cost: 10_000_000_000, + count: 25, + cost: 10_000_000_000 }), lunarPower: basic({ type: 'moon', description: 'Out with the sol, in with the lun!', count: 1, - cost: 3_300_000_000, + cost: 3_300_000_000 }), womanOnTheMoon: basic({ type: 'moon', description: 'There\'s no reason for it not to be a woman!', count: 10, - cost: 39_700_000_000, + cost: 39_700_000_000 }), doubleCraters: basic({ type: 'moon', description: 'Making every side look like the dark side.', - count: 25, - cost: 165_000_000_000, + count: 25, + cost: 165_000_000_000 }), glassButterfly: basic({ type: 'butterfly', description: 'Not your grandma\'s universe manipulation.', count: 1, - cost: 51_000_000_000, + cost: 51_000_000_000 }), monarchMigration: basic({ type: 'butterfly', description: 'This upgrade brought to you by milkweed.', count: 10, - cost: 870_000_000_000, + cost: 870_000_000_000 }), quadWing: basic({ type: 'butterfly', description: 'Sounds a lot like a trillion bees buzzing inside your head.', - count: 25, - cost: 2_550_000_000_000, + count: 25, + cost: 2_550_000_000_000 }), silverMirror: basic({ type: 'mirror', description: 'Excellent for stabbing vampires.', count: 1, - cost: 750_000_000_000, + cost: 750_000_000_000 + }), + pocketMirror: basic({ + type: 'mirror', + description: 'Take your self-reflection on the go!', + count: 10, + cost: 18_000_000_000_000 }), window: basic({ type: 'mirror', description: 'Only through looking around you can you acquire the self reflection necessary to control the thermostat.', - count: 25, - cost: 37_500_000_000_000, + count: 25, + cost: 37_500_000_000_000 }), fzero: basic({ type: 'quade', description: 'Brings out his competitive spirit.', count: 1, - cost: 10_000_000_000_000, + cost: 10_000_000_000_000 + }), + triHumpCamel: basic({ + type: 'quade', + description: 'YEE HAW :trimedary_camel:', + count: 10, + cost: 200_000_000_000_000 }), adam: basic({ type: 'quade', description: 'He could probably reach the thermostat if he wanted.', - count: 25, - cost: 500_000_000_000_000, + count: 25, + cost: 500_000_000_000_000 }), latestNode: basic({ type: 'hvacker', description: 'The old one has terrible ergonomics, tsk tsk.', - count: 1, - cost: 140_000_000_000_000, + count: 1, + cost: 140_000_000_000_000 + }), + nativeFunctions: basic({ + type: 'hvacker', + description: 'Sometimes javascript just isn\'t fast enough.', + count: 10, + cost: 3_300_000_000_000_000 }), gitCommits: basic({ type: 'hvacker', description: 'The heads of multiple people in a company are better than, for example, merely one head.', - count: 25, - cost: 7_000_000_000_000_000, + count: 25, + cost: 7_000_000_000_000_000 }), homage: { @@ -204,7 +222,7 @@ module.exports = { condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 200, emoji: 'cookie', cost: 10_000_000_000, - effect: (itemCps, user) => Math.ceil(itemCps * 1.1) + effect: (itemCps, user) => itemCps * 1.1 }, iLoveHvac: { type: 'general', @@ -212,7 +230,7 @@ module.exports = { condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400, emoji: 'heart', cost: 100_000_000_000_000, - effect: (itemCps, user) => Math.ceil(itemCps * 1.1) + effect: (itemCps, user) => itemCps * 1.1 } // moreUpgrades: { @@ -224,4 +242,3 @@ module.exports = { // effect: nothing // }, } - diff --git a/src/games/hvacoins/utils.js b/src/games/hvacoins/utils.js index 80f94d7..893f9fc 100644 --- a/src/games/hvacoins/utils.js +++ b/src/games/hvacoins/utils.js @@ -1,20 +1,38 @@ const fs = require('fs') -const jokes = require('../jokes') -const achievements = require('./achievements'); -const buyableItems = require("./buyableItems"); -const upgrades = require("./upgrades"); -const quackStore = require("./quackstore"); +//const jokes = require('../jokes') +const achievements = require('./achievements') +const buyableItems = require('./buyableItems') +const upgrades = require('./upgrades') +const { quackStore, getChaos } = require('./quackstore') const saveFile = 'hvacoins.json' const logError = msg => msg ? console.error(msg) : () => { /* Don't log empty message */ } -const loadGame = () => parseOr(fs.readFileSync('./' + saveFile, 'utf-8'), - () => ({ - users: {}, - nfts: [], - squad: {} - })) +const loadGame = () => { + const game = parseOr(fs.readFileSync('./' + saveFile, 'utf-8'), + () => ({ + users: {}, + nfts: [], + squad: {}, + horrors: {} + })) + game.horrors ??= {} + return game +} + +const chaosFilter = (num, odds, user, max = Infinity) => { + const userQuackgrades = user.quackUpgrades?.cps || [] + const hasChaos = userQuackgrades.includes('chaos') + if (!hasChaos || Math.random() < odds) { + return num + } + const chaosed = num * getChaos(user) + if (chaosed > max) { + return max + } + return chaosed +} const parseOr = (parseable, orFunc) => { try { @@ -25,10 +43,16 @@ const parseOr = (parseable, orFunc) => { } } +const makeBackup = () => { + const fileName = './backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_') + console.log(`Making backup file: ${fileName}`) + fs.writeFileSync(fileName, JSON.stringify(game)) +} + let saves = 0 const saveGame = () => { if (saves % 100 === 0) { - fs.writeFileSync('./backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_'), JSON.stringify(game)) + makeBackup() } saves += 1 fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2)) @@ -54,7 +78,82 @@ const idFromWord = word => { const getSeconds = () => new Date().getTime() / 1000 -const commas = num => num.toLocaleString() +const bigNumberWords = [ + ['tredecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000], + ['duodecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000], + ['undecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000], + ['decillion', 1_000_000_000_000_000_000_000_000_000_000_000], + ['nonillion', 1_000_000_000_000_000_000_000_000_000_000], + ['octillion', 1_000_000_000_000_000_000_000_000_000], + ['septtillion', 1_000_000_000_000_000_000_000_000], + ['sextillion', 1_000_000_000_000_000_000_000], + ['quintillion', 1_000_000_000_000_000_000], + ['quadrillion', 1_000_000_000_000_000], + ['trillion', 1_000_000_000_000], + ['billion', 1_000_000_000], + ['million', 1_000_000], +] + +const commas = (num, precise = false) => { + num = Math.round(num) + const bigNum = bigNumberWords.find(([, base]) => num >= base) + if (bigNum && !precise) { + const [name, base] = bigNum + const nummed = (num / base).toPrecision(3) + return `${nummed} ${name}` + } + return num.toLocaleString() +} + +const parseAll = (str, allNum) => { + if (!str) { + return NaN + } + + str = str.toLowerCase()?.replace(/,/g, '') + + if (str === 'all') { + return allNum + } + + switch (str) { + case 'my soul': + return allNum + case 'sex': + case 'sex number': + return 69_000_000 + case ':maple_leaf:': + case ':herb:': + case 'weed': + case 'weed number': + return 420_000_000 + case 'a milli': + return 1_000_000 + case 'a band': + return 1000 + case ':100:': + case 'one hunna': + return 100 + } + + console.log('STR', str) + if (str.match(/^\d+$/)) { + console.log('parseInt()') + return parseInt(str) + } + + if (str.match(/^\d+\.\d+$/)) { + console.log('parseFloat()') + return Math.round(parseFloat(str)) + } + + const bigNum = bigNumberWords.find(([name]) => str.endsWith(name)) + if (bigNum && str.match(/^\d+(\.\d+)?/)) { + return Math.round(parseFloat(str) * bigNum[1]) + } + + return NaN +} const game = loadGame() const { users, nfts, squad } = game @@ -107,7 +206,7 @@ const getCoins = userId => { const lastCheck = user.lastCheck || currentTime const secondsPassed = currentTime - lastCheck - const increase = getCPS(userId) * secondsPassed + const increase = getCPS(user) * secondsPassed user.coins += increase user.coinsAllTime += increase user.coins = Math.floor(user.coins) @@ -118,8 +217,7 @@ const getCoins = userId => { return user.coins } -const getCPS = userId => { - const user = getUser(userId) +const getCPS = user => { const userItems = user?.items || {} return Math.round(Object.keys(userItems).reduce((total, itemName) => total + getItemCps(user, itemName), 0)) } @@ -130,14 +228,14 @@ const squadUpgrades = { tastyKeyboards: { name: 'Tasty Keyboards', description: 'Delicious and sticky. Boosts CPS by 20% for everyone.', - effect: cps => Math.ceil(cps * 1.2), + effect: cps => cps * 1.2, cost: 10_000_000_000_000, emoji: 'keyboard' }, copyPasteMacro: { name: 'Copy-Paste Macro.', description: 'Don\'t actually use this. Boosts CPS by 20% for everyone.', - effect: cps => Math.ceil(cps * 1.2), + effect: cps => cps * 1.2, cost: 100_000_000_000_000, emoji: 'printer' } @@ -147,35 +245,122 @@ const squadHas = ([name]) => squad.upgrades[name] === true const squadIsMissing = name => !squadHas(name) const getCompletedSquadgrades = () => - Object.entries(squadUpgrades) - .filter(squadHas) - .map(([, upgrade]) => upgrade) + Object.entries(squadUpgrades) + .filter(squadHas) + .map(([, upgrade]) => upgrade) + +const prestigeMultiplier = user => 1 + ((user.prestige || 0) * 0.01) + +const quackGradeMultiplier = user => { + const userQuackgrades = user.quackUpgrades?.cps || [] + return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1) +} const singleItemCps = (user, itemName) => { const baseCps = buyableItems[itemName].earning + // console.log('') + // console.log(`${itemName} CPS:`) + // console.log('baseCps', baseCps) const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name]) - const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), baseCps) + const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), 1) + // console.log('itemUpgradeCps', itemUpgradeCps) const userGeneralUpgrades = user.upgrades.general || [] - const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), itemUpgradeCps) + const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), 1) + // console.log('generalUpgradeCps', generalUpgradeCps) const achievementCount = Object.keys(user.achievements || {}).length const achievementMultiplier = Math.pow(1.01, achievementCount) + // console.log('achievementMultiplier', achievementMultiplier) - const userQuackgrades = user.quackUpgrades?.cps || [] - const quackMultiplier = userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1) + const quackGrade = quackGradeMultiplier(user) + // console.log('quackgrade', quackGrade) - const prestigeMultiplier = 1 + ((user.prestige || 0) * 0.01) + const pMult = prestigeMultiplier(user) + // console.log('prestigeMultiplier', pMult) - return achievementMultiplier * - quackMultiplier * - prestigeMultiplier * - getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), generalUpgradeCps) + const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1) + // console.log('squadGradeMultiplier', squadGradeMultiplier) + + const total = + baseCps * + achievementMultiplier * + itemUpgradeCps * + generalUpgradeCps * + quackGrade * + pMult * + squadGradeMultiplier + + // console.log('Single Item CPS:', total) + + return total } +const shuffle = str => str.split('').sort(() => 0.5 - Math.random()).join('') + +const shufflePercent = (str, percentOdds) => { + const shuffled = shuffle(str) + let partiallyShuffled = '' + const shuffleChar = () => Math.random() < percentOdds + + let isEmoji = false + for (let i = 0; i < str.length; i++) { + if (str[i] === ':') { + isEmoji = !isEmoji + } + if (isEmoji) { // Less likely to shuffle emojis + partiallyShuffled += (shuffleChar() && shuffleChar()) ? shuffled[i] : str[i] + } else { + partiallyShuffled += shuffleChar() ? shuffled[i] : str[i] + } + } + + return partiallyShuffled +} + +const definitelyShuffle = (str, percentOdds) => { + if (!str) { + return str + } + if (!percentOdds) { + percentOdds = 0.01 + } + let shuffled = str + while (shuffled === str) { + shuffled = shufflePercent(str, percentOdds) + } + return shuffled +} + +const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)] + +/** + * Adds reactions to the given message, in order. + * If adding any reaction is a failure, it will continue on to the next. + * + * @param app The slack bolt app + * @param channelId The id of the channel the message is in + * @param timestamp The timestamp of the message + * @param reactions An array of reactions to add + * @returns {Promise} + */ +const addReactions = async ({ app, channelId, timestamp, reactions }) => { + for (const reaction of reactions) { + try { + await app.client.reactions.add({ + channel: channelId, + timestamp, + name: reaction + }) + } catch (e) { + logError(e) + } + } +} module.exports = { saveGame, + makeBackup, logError, parseOr, maybeNews, @@ -190,5 +375,13 @@ module.exports = { getItemCps, squadUpgrades, squadIsMissing, + prestigeMultiplier, + quackGradeMultiplier, + shufflePercent, + definitelyShuffle, + parseAll, + getRandomFromArray, + chaosFilter, + addReactions, game -} \ No newline at end of file +} diff --git a/src/games/hvacoins/webapi.js b/src/games/hvacoins/webapi.js index bb11384..ff899d0 100644 --- a/src/games/hvacoins/webapi.js +++ b/src/games/hvacoins/webapi.js @@ -46,10 +46,10 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => { console.log(' rate limited') return } - console.log(` went through for ${slack.ourUsers[event.user]}`) + console.log(` went through for ${slack.users[event.user]}`) userGetter.users[event.user].lastApiCall = currentTime - await action({event, say, words}) + await action({ event, say, words }) } catch (e) { console.error(e) await say(e.stack) @@ -66,4 +66,4 @@ module.exports = { launch: () => app.listen(port, () => { console.log(`Express listening on port ${port}`) }) -} \ No newline at end of file +} diff --git a/src/games/routine.js b/src/games/routine.js index 881aeac..76d6b7f 100644 --- a/src/games/routine.js +++ b/src/games/routine.js @@ -22,7 +22,9 @@ const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, in if (event.channel_type === 'im') { const eventText = event.text?.toLowerCase() if (eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword))) { + console.log('Trigger found') const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0] + console.log('Messaging opponent ' + slack.users[opponent.substring(2, opponent.length - 1)]) const msg = messageFromBoard({ dataName, gameName, @@ -50,7 +52,8 @@ const getMessages = winner => { } const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turnChoiceEmojis, makeMove }) => async ({ event, say }) => { - if (event.item_user !== slack.hvackerBotUserId || !turnChoiceEmojis.includes(event.reaction)) { + if (event.item_user !== slack.users.Hvacker || !turnChoiceEmojis.includes(event.reaction)) { + console.log('bad item_user/reaction') return } @@ -61,12 +64,14 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn const game = decodeGame(dataName, message.messages[0].text) if (!game) { + console.log('could not decode game') return } const { board, players } = game let winner = checkWinner(board) if (winner) { + console.log('winner found: ' + winner) return } @@ -101,6 +106,7 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn name: emojiName }) turnChoiceEmojis.forEach(removeEmoji) + console.log('SENDING to ' + opponent) const sentBoard = await slack.app.client.chat.postMessage({ channel: opponent, text: boardMessage + winnerMessages.opponent diff --git a/src/games/tictactoe.js b/src/games/tictactoe.js index c5d178b..add7d99 100644 --- a/src/games/tictactoe.js +++ b/src/games/tictactoe.js @@ -1,12 +1,12 @@ -const routine = require("./routine"); +const routine = require('./routine') const emptyBoard = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '] const textFromBoard = board => ` ${board[0]} | ${board[1]} | ${board[2]} \n` + - `-----------\n` + + '-----------\n' + ` ${board[3]} | ${board[4]} | ${board[5]} \n` + - `-----------\n` + + '-----------\n' + ` ${board[6]} | ${board[7]} | ${board[8]}` const numEmojis = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] @@ -21,7 +21,7 @@ const winningThrees = [ [2, 5, 8], [0, 4, 8], - [2, 4, 6], + [2, 4, 6] ] const checkWinner = board => { @@ -71,4 +71,3 @@ routine.build({ makeMove: applyTurn, checkWinner }) - diff --git a/src/index.js b/src/index.js index 9c8dfe5..3884f8e 100644 --- a/src/index.js +++ b/src/index.js @@ -26,12 +26,12 @@ onTempChangeRequested(change => { case 'Hotter': { lowTemp += 2 highTemp += 2 - break; + break } case 'Colder': { lowTemp -= 2 highTemp -= 2 - break; + break } case 'Good': { return @@ -42,9 +42,9 @@ onTempChangeRequested(change => { lowTemp = cleanTemp(lowTemp) const mode = - indoorTemperature < lowTemp ? heatMode : // Heat if lower than low - indoorTemperature > highTemp ? coolMode : // Cool if hotter than high - change === 'Hotter' ? heatMode : coolMode // Otherwise (lower priority) follow the requested change + indoorTemperature < lowTemp ? heatMode // Heat if lower than low + : indoorTemperature > highTemp ? coolMode // Cool if hotter than high + : change === 'Hotter' ? heatMode : coolMode // Otherwise (lower priority) follow the requested change if (!mode) { return diff --git a/src/slack/index.js b/src/slack/index.js index 734cc40..f282510 100644 --- a/src/slack/index.js +++ b/src/slack/index.js @@ -1,9 +1,8 @@ const { App: SlackApp } = require('@slack/bolt') const config = require('../config') +const { addReactions } = require('../games/hvacoins/utils') const temperatureChannelId = 'C034156CE03' -const hvackerBotUserId = 'U0344TFA7HQ' -const sageUserId = 'U028BMEBWBV' const pollingMinutes = 5 const pollingPeriod = 1000 * 60 * pollingMinutes @@ -41,6 +40,7 @@ const sendHelp = async (say, prefix) => { await say({ text: prefix + `Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` + + '\'Hotter\' and \'Colder\' votes offset. E.g. with votes Hotter - 4, Colder - 3, and Content - 2, the temp won\'t change.\n' + 'At this time I am not capable of actually changing the temperature. Go bug Quade.' }) } @@ -58,22 +58,38 @@ app.event('reaction_added', async ({ event, context, client, say }) => { } }) -const ourUsers = { +const users = { U028BMEBWBV: 'Sage', U02U15RFK4Y: 'Adam', U02AAB54V34: 'Houston', U02KYLVK1GV: 'Quade', U017PG4EL1Y: 'Max', UTDLFGZA5: 'Tyler', - U017CB5L1K3: 'Andres' + U017CB5L1K3: 'Andres', + U0344TFA7HQ: 'Hvacker', + U0X0ZQCN6: 'Caleb', + U03BBTD4CQZ: 'Fernando', + + Sage: 'U028BMEBWBV', + Adam: 'U02U15RFK4Y', + Houston: 'U02AAB54V34', + Quade: 'U02KYLVK1GV', + Max: 'U017PG4EL1Y', + Tyler: 'UTDLFGZA5', + Andres: 'U017CB5L1K3', + Caleb: 'U0X0ZQCN6', + Hvacker: 'U0344TFA7HQ', + Fernando: 'U03BBTD4CQZ', } const activePolls = {} const testId = 'U028BMEBWBV_TEST' let testMode = false app.event('message', async ({ event, context, client, say }) => { - console.log(event) - if (event.user === sageUserId) { + if (event.subtype !== 'message_changed') { + console.log(event) + } + if (event?.user === users.Sage) { if (event?.text.startsWith('!')) { if (testMode) { await messageSage('Currently in test mode!') @@ -89,13 +105,14 @@ app.event('message', async ({ event, context, client, say }) => { if (testMode) { event.user = testId } - // console.log(event.blocks[0].elements[0]) } for (const listener of messageListeners) { listener({ event, say }) } - console.log('MSG', ourUsers[event.user], "'" + event.text + "'", new Date().toLocaleTimeString()) - if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') { + if (event.user) { + console.log('MSG', users[event.user], "'" + event.text + "'", new Date().toLocaleTimeString()) + } + if (event.user === users.Sage && event.channel === 'D0347Q4H9FE') { if (event.text === '!!kill') { process.exit() } @@ -103,18 +120,10 @@ app.event('message', async ({ event, context, client, say }) => { await postToTechThermostatChannel(event.text.substring(4).trim()) return } - if (event.text?.startsWith('!saytoq ')) { - await messageQuade(event.text.substring(7).trim()) - return - } - if (event.text?.startsWith('!saytos')) { - await messageSage(event.text.substring(7).trim()) - return - } } const eventText = event.text?.toLowerCase() || '' - if (eventText.startsWith('!help')) { + if (eventText === '!help') { await sendHelp(say) return } @@ -139,21 +148,51 @@ app.event('message', async ({ event, context, client, say }) => { timestamp: pollTs, full: true }) + + const reactPosters = {} + reactions.message.reactions.forEach(r => r.users.forEach(user => { + reactPosters[user] ??= [] + reactPosters[user].push(r.name) + })) + const reactCounts = {} - reactions.message.reactions.forEach(reaction => { reactCounts[reaction.name] = reaction.count }) + Object.entries(reactPosters).forEach(([id, votes]) => { + votes = votes.filter(v => [goodEmoji, hotterEmoji, colderEmoji].includes(v)) + if (votes.length === 1) { + reactCounts[votes[0]] ??= 0 + reactCounts[votes[0]] += 1 + } + }) - const contentVotes = reactCounts[goodEmoji] - const hotterVotes = reactCounts[hotterEmoji] - const colderVotes = reactCounts[colderEmoji] + const contentVotes = reactCounts[goodEmoji] || 0 + let hotterVotes = reactCounts[hotterEmoji] || 0 + let colderVotes = reactCounts[colderEmoji] || 0 + console.log('before contentVotes', contentVotes) + console.log('before colderVotes', colderVotes) + console.log('before hotterVotes', hotterVotes) - let text = 'The people have spoken, and would like to ' + if (hotterVotes > colderVotes) { + hotterVotes -= colderVotes + colderVotes = 0 + } else if (colderVotes > hotterVotes) { + colderVotes -= hotterVotes + hotterVotes = 0 + } + console.log('after contentVotes', contentVotes) + console.log('after colderVotes', colderVotes) + console.log('after hotterVotes', hotterVotes) + + let text if (hotterVotes > colderVotes && hotterVotes > contentVotes) { + text = `<@${users.Quade}> The people have spoken, and would like to ` text += 'raise the temperature, quack.' requestTempChange('Hotter') } else if (colderVotes > hotterVotes && colderVotes > contentVotes) { + text = `<@${users.Quade}> The people have spoken, and would like to ` text += 'lower the temperature, quack quack.' requestTempChange('Colder') } else { + text = `The people have spoken, and would like to ` text += 'keep the temperature as-is, quaaack.' requestTempChange('Good') } @@ -166,9 +205,6 @@ app.event('message', async ({ event, context, client, say }) => { ;(async () => { await app.start().catch(console.error) console.log('Slack Bolt has started') - // setTimeout(async () => { - // await messageSage('') - // }, 2000) })() const postToTechThermostatChannel = async optionsOrText => { @@ -180,8 +216,7 @@ const postToTechThermostatChannel = async optionsOrText => { return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId }) } -const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText) -const messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText) +const messageSage = async optionsOrText => messageIn(users.Sage, optionsOrText) const messageIn = async (channel, optionsOrText) => { if (optionsOrText === null || typeof optionsOrText !== 'object') { @@ -196,17 +231,14 @@ const startPoll = async () => { const sent = await postToTechThermostatChannel({ text: ` Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted.\n` + `Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is.` + - `\n(Note that I can't actually change the temperature yet. Make Quade do it!)` + '\n(Note that I can\'t actually change the temperature yet. Make Quade do it!)' + }) + await addReactions({ + app, + channelId: temperatureChannelId, + timestamp: sent.ts, + reactions: [colderEmoji, hotterEmoji, goodEmoji] }) - const addReaction = async emojiName => - app.client.reactions.add({ - channel: temperatureChannelId, - timestamp: sent.ts, - name: emojiName - }) - await addReaction(colderEmoji) - await addReaction(hotterEmoji) - await addReaction(goodEmoji) return sent.ts } @@ -234,19 +266,18 @@ const decodeData = (key, message) => { const onReaction = listener => reactionListeners.push(listener) onReaction(async ({ event }) => { - if (event.user === sageUserId && event.reaction === 'x') { - console.log(event) - try { - await app.client.chat.delete({channel: event.item.channel, ts: event.item.ts}) - } catch (e) { - console.error(e) + if (event.user === users.Sage) { + if (event.reaction === 'x') { + try { + await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts }) + } catch (e) { + } } } }) module.exports = { app, - hvackerBotUserId, temperatureChannelId, onAction: app.action, getMessage, @@ -257,10 +288,9 @@ module.exports = { onReaction, encodeData, decodeData, - sageUserId, messageSage, messageIn, testMode, testId, - ourUsers + users }