diff --git a/package-lock.json b/package-lock.json index 1797764..bf705db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -673,6 +673,16 @@ "promise.allsettled": "^1.0.2", "raw-body": "^2.3.3", "tsscmp": "^1.0.6" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + } } }, "@slack/logger": { @@ -1082,11 +1092,11 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz", + "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.8" } }, "babel-jest": { diff --git a/package.json b/package.json index 77754e2..41ea47e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "license": "UNLICENSED", "dependencies": { "@slack/bolt": "^3.9.0", + "axios": "^0.26.0", "base-64": "^1.0.0", "express": "^4.17.3", "fs": "0.0.1-security", diff --git a/src/auth/index.js b/src/auth/index.js index 6ae70c5..c671447 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,5 +1,5 @@ -let base64 = require('base-64') -let config = require('../config') +const base64 = require('base-64') +const config = require('../config') const url = '' diff --git a/src/games/hvacoins/achievements.js b/src/games/hvacoins/achievements.js index 94c220c..12f6612 100644 --- a/src/games/hvacoins/achievements.js +++ b/src/games/hvacoins/achievements.js @@ -38,6 +38,42 @@ module.exports = { name: 'You light my fire, baby', description: 'And you pay attention to descriptions!', emoji: 'fire' + }, + + ratGod: { + name: 'Own 100 Mice', + description: 'I\'m beginning to feel like a rat god, rat god.', + emoji: 'mouse2' + }, + weAllNeedHelp: { + name: `View the '!coin' help`, + description: 'We all need a little help sometimes', + emoji: 'grey_question' + }, + showReverence: { // Not implemented + name: 'Show your reverence in the chat', + description: 'What a good little worshipper.', + emoji: 'blush' + }, + walmartGiftCard: { + name: 'Walmart Gift Card', + description: 'May or may not be expired', + emoji: 'credit_card' + }, + + hvackerAfterDark: { + name: 'Hvacker after dark', + description: 'You might be taking this a little far.', + emoji: 'night_with_stars' + }, + certifiedCoolGuy: { + name: 'Certified Cool Guy', + description: 'You absolutely know how to party.', + emoji: 'sunglasses' + }, + youDisgustMe: { + name: 'You disgust me', + description: 'Like, wow.', + emoji: 'nauseated_face' } } - diff --git a/src/games/hvacoins/buy.js b/src/games/hvacoins/buy.js new file mode 100644 index 0000000..796509d --- /dev/null +++ b/src/games/hvacoins/buy.js @@ -0,0 +1,132 @@ +const buyableItems = require('./buyableItems'); +const { commas, saveGame, setHighestCoins, addAchievement, getCoins, getUser, singleItemCps } = require('./utils'); +const slack = require('../../slack') + +const calculateCost = ({ itemName, user, quantity = 1 }) => { + let currentlyOwned = user.items[itemName] || 0 + let realCost = 0 + for (let i = 0; i < quantity; i++) { + realCost += Math.ceil(buyableItems[itemName].baseCost * Math.pow(1.15, currentlyOwned || 0)) + currentlyOwned += 1 + } + return realCost +} + +const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => { + const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost) + const itemCps = Math.round(singleItemCps(user, itemName)) + return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(itemCps)} CPS\n_${description}_` +} +const canView = highestCoins => ([, item]) => item.baseCost < (highestCoins || 1) * 101 +const buyableText = (highestCoins, user) => Object.entries(buyableItems) + .filter(canView(highestCoins)) + .map(getItemHeader(user)) + .join('\n\n') + + '\n\n:grey_question::grey_question::grey_question:' + + '\n\nJust type \'!buy item_name\' to purchase' + +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}_` + }, + accessory: { + 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) + }) +} + +const buyRoute = async ({ event, say, words }) => { + const user = getUser(event.user) + const buying = words[1] + setHighestCoins(event.user) + if (!buying) { + const highestCoins = user.highestEver || user.coins || 1 + if (buyableItems.quade.baseCost < highestCoins * 100) { + addAchievement(user, 'seeTheQuade', say) + } + await say(buyText2(highestCoins, user)) + return + } + + const buyable = buyableItems[buying] + if (!buyable) { + await say('That item does not exist!') + return + } + + let quantity = 1 + const currentCoins = getCoins(event.user) + if (words[2] === 'max') { + while (calculateCost({ itemName: buying, user, quantity: quantity + 1 }) <= currentCoins) { + quantity++ + } + } else { + quantity = parseInt(words[2] || '1') + } + if (!quantity || quantity < 1) { + await say('Quantity must be a positive integer') + return + } + + const realCost = calculateCost({ itemName: buying, user, quantity }) + if (currentCoins < realCost) { + await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`) + return + } + 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() +} + +const buyButton = async ({ body, ack, say, payload }) => { + await ack() + const buying = payload.action_id.substring(4) + console.log(`buyButton ${buying} clicked`) + const event = { + user: body.user.id, + } + const user = getUser(event.user) + const words = ['', buying, '1'] + await buyRoute({ event, say, words }) + const highestCoins = user.highestEver || user.coins || 1 + await slack.app.client.chat.update({ + channel: body.channel.id, + ts: body.message.ts, + ...buyText2(highestCoins, user) + }) +} + +Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton)) + +module.exports = buyRoute \ No newline at end of file diff --git a/src/games/hvacoins/buyableItems.js b/src/games/hvacoins/buyableItems.js index 87dab69..acf59eb 100644 --- a/src/games/hvacoins/buyableItems.js +++ b/src/games/hvacoins/buyableItems.js @@ -64,5 +64,11 @@ module.exports = { earning: 65_000_000, emoji: 'hvacker_angery', description: 'Harness the power of the mad god himself.' - } + }, + creator: { + baseCost: 170_000_000_000_000, + earning: 430_000_000, + emoji: 'question', + description: 'The elusive creator of Hvacker takes a favorable look at your CPS.' + }, } diff --git a/src/games/hvacoins/index.js b/src/games/hvacoins/index.js index 9dd17ad..dac678c 100644 --- a/src/games/hvacoins/index.js +++ b/src/games/hvacoins/index.js @@ -1,9 +1,24 @@ +const { + saveGame, + logError, + getCPS, + squadUpgrades, + getItemCps, + squadIsMissing, + maybeNews, + idFromWord, + getCoins, + getUser, + commas, + addAchievement, + game: {nfts, squad, users} +} = require('./utils') const slack = require('../../slack') -const fs = require('fs') -const jokes = require('./../jokes') const buyableItems = require('./buyableItems') const upgrades = require('./upgrades') const achievements = require('./achievements') +const webapi = require('./webapi') +const prestige = require('./prestige') // const readline = require('readline').createInterface({ // input: process.stdin, @@ -19,608 +34,727 @@ const achievements = require('./achievements') // read() // })(); -const saveFile = 'hvacoins.json' - -const parseOr = (parseable, orFunc) => { - try { - return JSON.parse(parseable) - } catch (e) { - console.error(e) - return orFunc() - } -} - -const game = parseOr(fs.readFileSync('./' + saveFile, 'utf-8'), - () => ({ users: {}, nfts: [] })) -const { users, nfts } = game - -const logError = msg => msg ? console.error(msg) : () => {} - -let saves = 0 -const saveGame = () => { - if (saves % 100 === 0) { - fs.writeFileSync('./backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_'), JSON.stringify(game)) - } - saves += 1 - fs.writeFileSync('./' + saveFile, JSON.stringify(game)) -} - -const maybeNews = say => { - const random = Math.random() - if (random > 0.98) { - const prefixedSay = msg => console.log(`Sent news update: '${msg}'`) || say('_Breaking news:_\n' + msg) - setTimeout(() => jokes.newsAlert(prefixedSay).catch(logError), 3000) - } else if (random > 0.96) { - setTimeout(async () => await say('_Say have you heard this one?_'), 3000) - setTimeout(() => jokes.tellJoke(say).catch(logError), 4000) - } -} - -const commas = num => num.toLocaleString() - -const getItemHeader = user => ([itemName, {baseCost, description, earning, emoji}]) => { - const itemCost = commas(user ? calculateCost({itemName, user}) : baseCost) - return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(earning)} CPS\n_${description}_` -} -const helpText = (highestCoins, user) => Object.entries(buyableItems) - .filter(([, value]) => value.baseCost < (highestCoins || 1) * 101) - .map(getItemHeader(user)) - .join('\n\n') + - '\n\n:grey_question::grey_question::grey_question:' + - '\n\nJust type \'!buy item_name\' to purchase' + - '\n\nNote: Listed prices are _base costs_ and will increase as you buy more.' - const getUpgradeEmoji = upgrade => upgrade.emoji || buyableItems[upgrade.type].emoji -const upgradeText = user => { - const userDoesNotHave = ([upgradeName, upgrade]) => !hasUpgrade(user, upgrade, upgradeName) +const upgradeText = (user, showOwned = false) => { + const userDoesNotHave = ([upgradeName, upgrade]) => hasUpgrade(user, upgrade, upgradeName) === showOwned const userMeetsCondition = ([, upgrade]) => upgrade.condition(user) 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') + - '\n\n:grey_question::grey_question::grey_question:' + - '\n\nJust type \'!upgrade upgrade_name\' to purchase' + Object.entries(upgrades) + .filter(userDoesNotHave) + .filter(userMeetsCondition) + .map(format) + .join('\n\n') + + '\n\n:grey_question::grey_question::grey_question:' + + '\n\nJust type \'!upgrade upgrade_name\' to purchase' } -const getUser = userId => { - if (!users[userId]) { - users[userId] = { - coins: 0, - items: {}, - upgrades: {}, - achievements: {} - } - } else { - users[userId].items ??= {} - users[userId].upgrades ??= {} - users[userId].achievements ??= {} - } - return users[userId] -} - -const getSeconds = () => new Date().getTime() / 1000 - -const setHighestCoins = userId => { - const prevMax = users[userId].highestEver || 0 - if (prevMax < users[userId].coins) { - users[userId].highestEver = users[userId].coins - } -} - -const hasUpgrade = (user, upgrade, upgradeName) => user.upgrades[upgrade.type]?.includes(upgradeName) - -const addAchievement = (user, achievementName, say) => { - if (user.achievements[achievementName]) { - return - } - setTimeout(async () => { - user.achievements[achievementName] = true - saveGame() - await say(`You earned the achievement ${achievements[achievementName].name}!`) - }, 500) -} +const hasUpgrade = (user, upgrade, upgradeName) => !!user.upgrades[upgrade.type]?.includes(upgradeName) const alwaysAccessible = () => true -const adminOnly = userId => userId === slack.sageUserId +const adminOnly = { + hidden: true, + condition: ({ event, say }) => { + if (!event.user.startsWith(slack.sageUserId)) { + say(`This is an admin-only command!`) + return false + } + return true + } +} +const testOnly = { + hidden: true, + condition: ({ event }) => event.user.includes('TEST') +} +const dmsOnly = { + hidden: false, + condition: ({ event, say, words }) => { + if (event.channel_type !== 'im') { + say(`Please use ${words[0]} in DMs only!`) + return false + } + return true + } +} +const prestigeOnly = adminOnly // TODO ({ event }) => !!getUser(event.user).prestige const commands = new Map() let commandHelpText = '' -const command = (commandNames, helpText, action, hidden = false, condition = alwaysAccessible) => { +const defaultAccess = { hidden: false, condition: alwaysAccessible } +const command = (commandNames, helpText, action, { hidden, condition } = defaultAccess) => { if (!hidden) { commandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')} - ${helpText}\n` } - commandNames.forEach(name => commands.set(name, { + if (!condition) { + condition = alwaysAccessible + } + if (Array.isArray(condition)) { + const conditionList = condition + condition = arg => conditionList.every(c => c(arg)) + } + const c = { commandNames, helpText, action, condition, hidden - })) + } + webapi.addCommand(c) + commandNames.forEach(name => { + if (commands.get(name)) { + throw `Duplicate command '${name}' detected.` + } + commands.set(name, c) + }) } -command( - ['!help', '!h'], - 'List available commands', - async ({ say }) => await say('```' + commandHelpText + '```') -) - -slack.onMessage(async ({ event, say }) => { +const messageHandler = async ({ event, say }) => { const words = event?.text?.split(/\s+/) || [] const c = commands.get(words[0]) - if (!words[0]?.startsWith('!')) { - return + 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?.condition(event.user)) { - //await say(`Command '${words[0]}' not found`) + const canUse = await c?.condition({ event, say, words }) + if (!canUse) { + // await say(`Command '${words[0]}' not found`) return } if (words[1] === 'help') { - await say(c.helpText) + await say(c.commandNames.map(name => '`' + name + '`').join(', ') + ': ' + c.helpText) + if (c.commandNames.includes('!coin')) { + addAchievement(getUser(event.user), 'weAllNeedHelp', say) + } return } await c.action({ event, say, words }) +} + +slack.onMessage(async msg => { + try { + await messageHandler(msg) + } catch (e) { + logError(e) + } }) command( - ['!a', '!ach', '!achievements'], - 'List your glorious achievements', - async ({ event, say }) => { - if (event.channel_type !== 'im') { - await say('Please only use !ach in DMs!') - return - } - const user = getUser(event.user) + ['!cleanusers'], + 'Calls getUser() on all users, ensuring a valid state.', + async ({ say }) => { + Object.keys(users).forEach(userId => { getUser(userId) }) + return say('```Cleaning ' + JSON.stringify(Object.keys(users), null, 2) + '```') + }, adminOnly) - const achievementCount = Object.keys(user.achievements).length - const prefix = `You have ${achievementCount} achievements!\n\n` - const mult = (Math.pow(1.01, achievementCount) - 1) * 100 - const list = Object.keys(user.achievements) - .map(name => achievements[name]) - .map(({description, emoji, name}) => `:${emoji}: *${name}* - ${description}`) - .join('\n') - const postfix = achievementCount ? `\n\n_Achievements are boosting your CPS by ${mult.toPrecision(3)}%_` : '' - await say(prefix + list + postfix) - } +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) + user.pwHash = webapi.makeHash(words[1]) + await saveGame() + await say(`Password encoded as ${user.pwHash}`) + } + , { hidden: true }) + +command( + ['!help', '!h'], + 'List available commands', + async ({ say }) => say('```' + commandHelpText + '```') ) -const getItemCps = (user, itemName) => { - const achievements = Object.keys(user.achievements || {}).length - const achievementMultiplier = Math.pow(1.01, achievements) - const baseCps = (user.items[itemName] || 0) * buyableItems[itemName].earning - const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name]) - return achievementMultiplier * itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), baseCps) +const removeAchievement = async (user, name, say) => { + if (user.achievements[name]) { + user.achievements[name] = false + await say('Achievement removed!') + } else { + await say('That user doesn\'t have that achievement!') + } } +command( + ['!rach'], + 'Remove achievement', + async ({ say, words }) => { + const achName = words[1] + const target = idFromWord(words[2]) + await removeAchievement(getUser(target), achName, say) + }, adminOnly) + +command( + ['!a', '!ach', '!achievements'], + 'List your glorious achievements', + async ({ event, say }) => { + const user = getUser(event.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) + .map(name => achievements[name]) + .map(({ description, emoji, name }) => `:${emoji}: *${name}* - ${description}`) + .join('\n') + '\n\n' + } + + const postfix = achievementCount ? `_Achievements are boosting your CPS by ${mult.toPrecision(3)}%_` : '' + await say(prefix + list + postfix) + } +) + +const emojiLine2 = (itemName, countOwned) => + ((emoji = buyableItems[itemName].emoji) => countOwned < 5 + ? `:${emoji}:`.repeat(countOwned) + : `:${emoji}: x${countOwned}`)() + +const emojiLine3 = (itemName, countOwned) => + (countOwned < 5 + ? s => s.repeat(countOwned) + : s => s + ` x${countOwned}`) + (buyableItems[itemName].emoji) + const emojiLine = (itemName, countOwned) => countOwned < 5 - ? `:${buyableItems[itemName].emoji}:`.repeat(countOwned) - : `:${buyableItems[itemName].emoji}: x${countOwned}` + ? `:${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(getItemCps(getUser(userId), itemName)) + ' cps') - .join('\n') + Object.entries(users[userId]?.items || {}) + .map(([itemName, countOwned]) => emojiLine(itemName, countOwned) + ' - ' + commas(Math.round(getItemCps(getUser(userId), itemName))) + ' cps') + .join('\n') -const getCPS = userId => { - const user = getUser(userId) - const userItems = user?.items || {} - const userGeneralUpgrades = user?.upgrades?.general || [] - const cpsFromItems = Object.keys(userItems).reduce((total, itemName) => total + getItemCps(getUser(userId), itemName), 0) - return Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), cpsFromItems) -} - -const getCoins = userId => { - const user = getUser(userId) - const currentTime = getSeconds() - const lastCheck = user.lastCheck || currentTime - const secondsPassed = currentTime - lastCheck - user.coins += getCPS(userId) * secondsPassed - user.coins = Math.floor(user.coins) - - user.lastCheck = currentTime - setHighestCoins(userId) - saveGame() - return user.coins -} - -command(['!cps'], 'Display your current Coins Per Second', async ({ event, say }) => { - await say(`You are currently earning \`${commas(getCPS(event.user))}\` HVAC Coin per second.`) -}) +command(['!cps'], + 'Display your current Coins Per Second', + async ({ event, say }) => + say(`You are currently earning \`${commas(getCPS(event.user))}\` HVAC Coin per second.`)) 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) - 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` - await slack.messageSage(`${slack.ourUsers[event.user]} found a lucky gold coin worth ${commas(diff)} HVAC!`) - 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 !== 0 ? 's' : '') + '. Spend wisely.') - saveGame() + ['!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) } -) - -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) - - let n - if (words[1].toLowerCase() === 'all') { - if (user.coins === 0) { - await say('You don\'t have any coins!') - return - } - n = user.coins - } else { - n = parseInt(words[1]) - } - if (!n || n < 0) { - await say(`Invalid number '${n}'`) - return - } - if (user.coins < n) { - await say(`You don\'t have that many coins! You have ${commas(user.coins)}.`) - return - } - if (n > 100_000_000_000) { - addAchievement(user, 'bigBets', say) - } - user.coins -= n - let outcome - if (Math.random() > 0.5) { - user.coins += (2 * n) - outcome = 'won' - } else { - outcome = 'lost' - } - console.log(`They ${outcome}`) - await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`) - saveGame() + 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 } -) - -const calculateCost = ({ itemName, user, quantity = 1 }) => { - let currentlyOwned = user.items[itemName] || 0 - let realCost = 0 - for (let i = 0; i < quantity; i++) { - realCost += Math.ceil(buyableItems[itemName].baseCost * Math.pow(1.15, currentlyOwned || 0)) - currentlyOwned += 1 + user.coins += diff + await say(`${prefix}You now have ${commas(user.coins)} HVAC coin` + (c !== 1 ? 's' : '') + '. Spend wisely.') + saveGame() } - return realCost +) + +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) + + 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]) + } + 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)}.`) + } + if (n >= 100_000_000_000) { + addAchievement(user, 'bigBets', say) + } + user.coins -= n + let outcome + if (Math.random() > 0.5) { + user.coins += (2 * n) + outcome = 'won' + } else { + outcome = 'lost' + } + console.log(`They ${outcome}`) + await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`) + saveGame() + } +) + +command( + ['!buynft', '!bn'], + 'Acquire high-quality art\n' + + ' To use, say \'!buynft nft_name\'', + async ({ event, say, words }) => { + 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.' : '' + return say('No NFT with that name found' + suffix) + } + if (nft.owner) { + return say('Someone already owns that NFT!') + } + const c = getCoins(event.user) + if (c < nft.price) { + return say('You don\'t have enough coin for this nft') + } + + users[event.user].coins -= nft.price + nft.owner = event.user + saveGame() + await say('You bought ' + nft.name + '!') + } +) + +command( + ['!myupgrades', '!myu'], + 'List all the upgrades that you own.', + async ({ event, say }) => { + const user = getUser(event.user) + await say(upgradeText(user, true)) + }, dmsOnly) + +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) + const upgradeName = words[1] + if (!upgradeName) { + return say(upgradeText(user)) + } + const upgrade = upgrades[upgradeName] + if (!upgrade) { + return say('An upgrade with that name does not exist!') + } + if (!user.upgrades[upgrade.type]) { + user.upgrades[upgrade.type] = [] + } + if (hasUpgrade(user, upgrade, upgradeName)) { + return say('You already have that upgrade!') + } + if (!upgrade.condition(user)) { + return say('That item does not exist!') + } + const c = getCoins(event.user) + if (c < upgrade.cost) { + return say(`You don't have enough coins! You have ${commas(c)}, but you need ${commas(upgrade.cost)}`) + } + user.coins -= upgrade.cost + user.upgrades[upgrade.type].push(upgradeName) + await saveGame() + await say(`You bought ${upgradeName}!`) + }, dmsOnly) + +const getCurrentSquadgrade = () => { + const current = Object.entries(squadUpgrades).find(squadIsMissing) + if (!current) { + return current + } + const [name, upgrade] = current + if (!squad.upgrades[name]) { + squad.upgrades[name] = upgrade.cost + } + return { + name, + remaining: squad.upgrades[name], + emoji: upgrade.emoji, + description: upgrade.description + } +} + +const squadgradeText = ([name, { emoji, description }]) => + `:${emoji}: *${name}*\n_${description}_` + +const squadText = () => { + const current = getCurrentSquadgrade() + if (current) { + const currentUpgradeText = ({ name, remaining, emoji, description }) => + `:${emoji}: *${name}* - ${commas(remaining)} HVAC remaining.\n_${description}_` + return currentUpgradeText(current) + } + return 'No more squadgrades currently available.' + // const squadIsMissing = ([name, upgrade]) => squad.upgrades[name] === false + // return squadgradeText(Object.entries(squadUpgrades).find(squadIsMissing)) } command( - ['!buynft', '!bn'], - 'Acquire high-quality art\n' + - ' To use, say \'!buynft nft_name\'', - async ({ event, say, words }) => { - 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.' : '' - await say('No NFT with that name found' + suffix) - return - } - if (nft.owner) { - await say('Someone already owns that NFT!') - return - } - const c = getCoins(event.user) - if (c < nft.price) { - await say('You don\'t have enough coin for this nft') - return - } - - users[event.user].coins -= nft.price - nft.owner = event.user - saveGame() - await say('You bought ' + nft.name + '!') - } -) - -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 }) => { - if (event.channel_type !== 'im') { - await say('Please only use !upgrade in DMs!') - return - } - const user = getUser(event.user) - const upgradeName = words[1] - if (!upgradeName) { - await say(upgradeText(user)) - return - } - const upgrade = upgrades[upgradeName] - if (!upgrade) { - await say('An upgrade with that name does not exist!') - return - } - if (!user.upgrades[upgrade.type]) { - user.upgrades[upgrade.type] = [] - } - if (hasUpgrade(user, upgrade, upgradeName)) { - await say('You already have that upgrade!') - return - } - if (!upgrade.condition(user)) { - await say('That item does not exist!') - return - } - const c = getCoins(event.user) - if (c < upgrade.cost) { - await say(`You don't have enough coins! You have ${commas(c)}, but you need ${commas(upgrade.cost)}`) - return - } - user.coins -= upgrade.cost - user.upgrades[upgrade.type].push(upgradeName) - await saveGame() - await say(`You bought ${upgradeName}!`) + ['!squad', '!sq'], + 'Buy upgrades that help the whole team.\n' + + ' Say !squad to list squad upgrades.\n' + + ' Say \'!squad contrib_amount\' to make progress toward the current upgrade.', + async ({ event, say, words }) => { + if (!words[1]) { + return say(squadText()) } + const current = getCurrentSquadgrade() + 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}.`) + } + } + if (!amount || amount < 1) { + return say(`Invalid amount: '${words[1]}'`) + } + 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) { + squad.upgrades[current.name] = true + } + saveGame() + await say(`Thank you for your contribution of ${commas(amount)} HVAC! Current status:\n\n${squadText()}`) + } ) +const buyRoute = require('./buy') command( ['!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.', - async ({ event, say, words }) => { - const user = getUser(event.user) - const buying = words[1] - const quantity = parseInt(words[2] || '1') - if (!quantity || quantity < 1) { - await say('Quantity must be a positive integer') - return - } - setHighestCoins(event.user) - if (!buying) { - const highestCoins = user.highestEver || user.coins || 1 - if (buyableItems.quade.baseCost < highestCoins * 100) { - addAchievement(user, 'seeTheQuade', say) - } - await say(helpText(highestCoins, user)) - return - } - - const buyable = buyableItems[buying] - if (!buyable) { - await say('That item does not exist!') - return - } - const realCost = calculateCost({itemName: buying, user, quantity}) - const currentCoins = getCoins(event.user) - if (currentCoins < realCost) { - await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`) - return - } - user.coins -= realCost - user.items[buying] = user.items[buying] || 0 - user.items[buying] += quantity - if (quantity === 1) { - await say(`You bought one ${buying}`) - } else { - await say(`You bought ${quantity} ${buying}`) - } - saveGame() - } + buyRoute ) - command( - ['!check', '!ch'], - 'Check how many coins another player has', - async ({ say, words }) => { - const target = words[1] - if (!target?.startsWith('<@') || !target.endsWith('>')) { - await say('Target must be a valid @') - return - } - const targetId = target.substring(2, target.length - 1) - if (targetId !== 'U0344TFA7HQ') { - const coins = getCoins(targetId) - await say(`<@${targetId}> has ${commas(coins)} HVAC.`) - } else { - 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.`) - } + ['!check', '!ch'], + 'Check how many coins another player has', + async ({ say, words }) => { + const targetId = idFromWord(words[1]) + if (!targetId) { + return say('Target must be a valid @') } + + if (targetId !== 'U0344TFA7HQ') { + const coins = getCoins(targetId) + await say(`<@${targetId}> has ${commas(coins)} HVAC.`) + } else { + 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.`) + } + } ) - command( - ['!gift', '!give', '!gi'], - 'Donate coins to a fellow player\n' + + ['!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) - const [, target, amountText] = words - const amount = amountText === 'all' ? (getCoins(userId)) : parseInt(amountText) - if (!amount || amount < 0) { - await say('Amount must be a positive integer!') - return - } - if (!target?.startsWith('<@') || !target.endsWith('>')) { - await say('Target must be a valid @') - return - } - if (user.coins < amount) { - await say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`) - return - } - const targetId = target.substring(2, target.length - 1) - const targetUser = getUser(targetId) - user.coins -= amount - targetUser.coins += amount - await say(`Gifted ${commas(amount)} HVAC to <@${targetId}>`) + async ({ event, words, say }) => { + const userId = event.user + const user = getUser(userId) + const [, target, amountText] = words + const amount = amountText === 'all' ? getCoins(userId) : parseInt(amountText) + const targetId = idFromWord(target) + if (!amount || amount < 0) { + return say('Amount must be a positive integer!') } + if (!targetId) { + return say('Target must be a valid @') + } + if (user.coins < amount) { + return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`) + } + if (amountText === 'all' && slack.ourUsers[targetId] === 'Tyler') { + addAchievement(user, 'walmartGiftCard', say) + } + const targetUser = getUser(targetId) + user.coins -= amount + targetUser.coins += amount + await say(`Gifted ${commas(amount)} HVAC to <@${targetId}>`) + } ) command( - ['!status', '!s'], - 'Print your current CPS, HVAC balance, and owned items', - async ({ event, say }) => { - await say( + ['!status', '!s'], + 'Print your current CPS, HVAC balance, and owned items', + async ({ event, say }) => { + 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\n` - ) - } + `${collection(event.user)}\n\nCoins collected all-time: ${commas(Math.round(users[event.user].coinsAllTime))}\n\n` + ) + } ) command( - ['!nfts', '!nft', '!n'], - 'Show NFTs in the museum\n' + + ['!gimme'], + 'Give self x coins', + async ({ event, say, words }) => { + const user = getUser(event.user) + const increase = parseInt(words[1].replace(/,/g, '')) + user.coins += increase + user.coinsAllTime += increase + await say(`You now have ${getCoins(event.user)} HVAC.`) + }, testOnly) + +command( + ['!nfts', '!nft', '!n'], + 'Show NFTs in the museum\n' + ' Call with no arguments to list all NFTs, or \'!nft nft_name\' to show a particular one.', - async ({ say, words }) => { - const owner = nft => `Owner: *${slack.ourUsers[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)}` + async ({ say, words }) => { + const owner = nft => `Owner: *${slack.ourUsers[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 + const filter = words[1] ? nft => words[1]?.toLowerCase() === nft.name : null - await say(nfts - .filter(filter || (() => true)) - .map(nftDisplay) - .join('\n-------------------------\n') || (filter ? 'No NFTs with that name exist' : 'No NFTs currently exist.') - ) - } + await say(nfts + .filter(filter || (() => true)) + .map(nftDisplay) + .join('\n-------------------------\n') || (filter ? 'No NFTs with that name exist' : 'No NFTs currently exist.') + ) + } ) command( - ['!leaderboard', '!lb'], - 'Show the top HVAC-earners, ranked by CPS', - async ({ event, say }) => { - const user = getUser(event.user) - 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') + '```').then(() => addAchievement(user, 'leaderBoardViewer', say)) - } + ['!leaderboard', '!lb'], + 'Show the top HVAC-earners, ranked by CPS', + async ({ event, say }) => { + const user = getUser(event.user) + 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') + '```' + ).then(() => addAchievement(user, 'leaderBoardViewer', say)) + } ) -command(['!pog'], 'Displays a poggers hvacker', async ({ say }) => { - await say('') -}, true) +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 }) -command(['!ligma'], 'GRRRR', async ({ say }) => { - await say(':hvacker_angery:') -}, 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('!dab', 'ACTIVATE COOL GUY MODE', '', 'certifiedCoolGuy') +oneShot('!based', 'Sorry, it\'s a little hard to hear you!', '') +oneShot('!shrek', 'Love and life, baby.', '') command( - ['!addnft'], - 'Arguments 1 and 2 should be on the first line as name (one word!) and integer price.\n' + + ['!addnft'], + 'Arguments 1 and 2 should be on the first line as name (one word!) and integer price.\n' + ' The second line should be the description of the pieces\n' + ' the picture is everything after the second line', - async ({ event }) => { - const [, name, price] = event.text.substring(0, event.text.indexOf('\n')).split(' ') - const rest = event.text.substring(event.text.indexOf('\n') + 1) - const desc = rest.substring(0, rest.indexOf('\n')) - const picture = rest.substring(rest.indexOf('\n') + 1) - const newNft = { - name, - price: parseInt(price.replace(/,/g, '')), - description: desc, - picture, - owner: null - } - nfts.push(newNft) - console.log('addedNft', newNft) - return saveGame() - }, true, adminOnly) - -command( - ['!ss'], - 'Show the status for another player: !ss @player', - async ({ words, say }) => { - const target = words[1] - const targetId = target.substring(2, target.length - 1) - await say( - `${target} are currently earning \`${commas(getCPS(targetId))}\` HVAC Coin per second.\n\n` + - `They currently have ${commas(getCoins(targetId))} HVAC Coins\n\n` + - `${collection(targetId)}\n\n` - ) - }, true, adminOnly) - -command( - ['!steal', '!sagesteal'], - 'Highly illegal', - async ({ event, words, say }) => { - const userId = event.user - const user = getUser(userId) - let [, target] = words - const amount = getCoins(userId) / 100 - if (!amount || amount < 0) { - return - } - let targetId - if (!target?.startsWith('<@') || !target.endsWith('>')) { - targetId = slack.sageUserId - } else { - targetId = target.substring(2, target.length - 1) - } - if (user.coins < amount) { - return - } - 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]}`) - }, true) - -command( - ['!lottery'], - 'Once per day, try for a big win!', - async ({ event, say }) => { - const user = getUser(event.user) - addAchievement(user, 'ignited', say) - }, true) - -command( - ['!ignite'], - 'You found me!', - async ({ event, say }) => { - const user = getUser(event.user) - addAchievement(user, 'ignited', say) + async ({ event }) => { + const [, name, price] = event.text.substring(0, event.text.indexOf('\n')).split(' ') + const rest = event.text.substring(event.text.indexOf('\n') + 1) + const desc = rest.substring(0, rest.indexOf('\n')) + const picture = rest.substring(rest.indexOf('\n') + 1) + const newNft = { + name, + price: parseInt(price.replace(/,/g, '')), + description: desc, + picture, + owner: null } -) + nfts.push(newNft) + console.log('addedNft', newNft) + return saveGame() + }, adminOnly) + command( - ['!giveach'], - '!giveach @player ach_name', - async ({ words, say }) => { - const targetId = words[1].substring(2, words[1].length - 1) - const user = getUser(targetId) - console.log(user, words[2], say) - addAchievement(user, words[2], say) - saveGame() - }, true, adminOnly) \ No newline at end of file + ['!prestige', '!p'], + 'Show your current prestige status', + prestige.prestigeRoute, + adminOnly +) + +command( + ['!!prestige'], + 'Confirm your prestige activation.', + prestige.prestigeConfirmRoute, + adminOnly +) + +command( + ['!quack', '!quackstore'], + 'Spend your prestigious quackings', + prestige.quackStoreRoute, + prestigeOnly +) + +command( + ['!ss'], + 'Show the status for another player: !ss @player', + async ({ words, say }) => { + const target = words[1] + const targetId = idFromWord(target) + await say( + `${target} is currently earning \`${commas(getCPS(targetId))}\` 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` + ) + }, adminOnly) + +const exec = require('child_process').exec; +command( + ['!pl'], + '!pl `code`', + async ({ event, say}) => { + const 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 = '```' + let errors = '' + child.stdout.on('data', data => result += data) + child.stderr.on('data', data => { + result += `\nERR:${data}\n` + errors += `\nERR:${data}` + }) + 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) + } + await say(result + '```') + }) + }, adminOnly) + +command( + ['!steal', '!sagesteal'], + 'Highly illegal', + async ({ event, words, say }) => { + const userId = event.user + const user = getUser(userId) + const [, target] = words + const amount = getCoins(userId) / 100 + if (!amount || amount < 0) { + return + } + let targetId = idFromWord(target) + if (!targetId) { + targetId = slack.sageUserId + } + if (user.coins < amount) { + return + } + 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]}`) + }, { hidden: true }) + +// TODO +command( + ['!lottery'], + 'Once per day, try for a big win!', + async ({ event, say }) => { + const user = getUser(event.user) + const currentDate = new Date().getDate() + const lastLotto = user.lastLotto || currentDate - 1 + if (lastLotto === currentDate) { + return say('Hey, only one lotto ticket per day, alright?') + } + user.lastLotto = currentDate + saveGame() + }, { hidden: true }) + +command( + ['!ignite'], + 'You found me!', + async ({ event, say }) => { + const user = getUser(event.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) + addAchievement(user, words[2], say) + saveGame() + }, adminOnly) + +command( + ['!whois'], + '!whois player_id', + async ({ words, say }) => say(`<@${words[1]}>`), + adminOnly) + +command( + ['!ngift'], + '!ngift player_id nft_name', + async ({ event, words, say }) => { + const targetId = idFromWord(words[1]) + if (!targetId) { + return say(`Please specify a valid @ target!`) + } + const nft = nfts.find(nft => nft.name === words[2] && nft.owner === event.user) + if (!nft) { + return say(`You don't own an nft with that name!`) + } + nft.owner = targetId + }, { hidden: true }) + +command( + ['!deletetest', '!dtest'], + '!deletetest', + async () => { + delete users[slack.testId] + saveGame() + }, adminOnly) + +command( + ['!message', '!msg', '!m'], + '!message player_id message', + async ({ event, words, say }) => { + const targetId = idFromWord(words[1]) + if (!targetId) { + 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 diff --git a/src/games/hvacoins/lottery.js b/src/games/hvacoins/lottery.js new file mode 100644 index 0000000..e69de29 diff --git a/src/games/hvacoins/prestige.js b/src/games/hvacoins/prestige.js new file mode 100644 index 0000000..210e509 --- /dev/null +++ b/src/games/hvacoins/prestige.js @@ -0,0 +1,125 @@ +const { getUser, getCoins, commas, saveGame } = require('./utils'); +const quackStore = require('./quackstore'); + +const possiblePrestige = coins => { + let p = 0 + while (tpcRec(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 => { + if (prestigeLevel === 0) { + return 0 + } + return (tpcRecMemo[prestigeLevel]) || (tpcRecMemo[prestigeLevel] = 1_000_000_000_000 * Math.pow(prestigeLevel, 3) + tpcRec(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' + + '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) + +// 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 quackStoreListing = ([name, upgrade]) => + `:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_` + +const allUserQuackUpgrades = user => + Object.entries(user.quackUpgrades || {}) + .map(([type, upgrades]) => upgrades) + +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 quackStoreRoute = async ({ event, say, words }) => { + const user = getUser(event.user) + user.quackUpgrades ??= {} + const quacks = user.quacks ??= 0 + if (!words[1]) { + await say(quackStoreText(user)) + return + } + const quackItem = quackStore[words[1]] + if (!quackItem) { + await say(`'${words[1]}' is not available in the quack store!`) + return + } + if (quackItem.cost > quacks) { + await say(`${words[1]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`) + return + } + user.quackUpgrades[quackItem.type] ??= [] + user.quackUpgrades[quackItem.type].push(words[1]) + saveGame() +} + +module.exports = { + quackStoreRoute, + prestigeRoute, + prestigeConfirmRoute +} diff --git a/src/games/hvacoins/quackstore.js b/src/games/hvacoins/quackstore.js new file mode 100644 index 0000000..2431c37 --- /dev/null +++ b/src/games/hvacoins/quackstore.js @@ -0,0 +1,20 @@ +const quackStore = { + ascent: { + name: 'Ascent', + type: 'cps', + emoji: 'rocket', + description: 'Welcome to level 2. Boosts all CPS by 20%', + effect: cps => cps * 1.2, + cost: 1 + }, + nuclearFuel: { + name: 'Nuclear Fuel', + type: 'cps', + emoji: 'atom_symbol', + description: 'The future is now. Boosts all CPS by 20%.', + preReqs: ['ascent'], + effect: cps => cps * 1.2, + cost: 5 + }, +} +module.exports = quackStore \ No newline at end of file diff --git a/src/games/hvacoins/upgrades.js b/src/games/hvacoins/upgrades.js index 09a92f5..3a3e7df 100644 --- a/src/games/hvacoins/upgrades.js +++ b/src/games/hvacoins/upgrades.js @@ -6,8 +6,6 @@ const basic = ({ type, description, count, cost }) => ({ effect: itemCps => itemCps * 2 }) -const nothing = itemCps => itemCps - module.exports = { doubleClick: basic({ type: 'mouse', @@ -173,6 +171,7 @@ module.exports = { count: 25, cost: 37_500_000_000_000, }), + fzero: basic({ type: 'quade', description: 'Brings out his competitive spirit.', @@ -186,6 +185,19 @@ module.exports = { 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, + }), + 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, + }), + homage: { type: 'general', description: 'The power of original ideas increases your overall CPS by 10%', @@ -194,6 +206,14 @@ module.exports = { cost: 10_000_000_000, effect: (itemCps, user) => Math.ceil(itemCps * 1.1) }, + iLoveHvac: { + type: 'general', + description: 'The power of love increases your overall CPS by 10%', + 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) + } // moreUpgrades: { // type: 'general', diff --git a/src/games/hvacoins/utils.js b/src/games/hvacoins/utils.js new file mode 100644 index 0000000..80f94d7 --- /dev/null +++ b/src/games/hvacoins/utils.js @@ -0,0 +1,194 @@ +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 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 parseOr = (parseable, orFunc) => { + try { + return JSON.parse(parseable) + } catch (e) { + logError(e) + return orFunc() + } +} + +let saves = 0 +const saveGame = () => { + if (saves % 100 === 0) { + fs.writeFileSync('./backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_'), JSON.stringify(game)) + } + saves += 1 + fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2)) +} + +const maybeNews = say => { + const random = Math.random() + if (random > 0.98) { + const prefixedSay = msg => console.log(`Sent news update: '${msg}'`) || say('_Breaking news:_\n' + msg) + setTimeout(() => jokes.newsAlert(prefixedSay).catch(logError), 3000) + } else if (random > 0.96) { + setTimeout(async () => say('_Say have you heard this one?_'), 3000) + setTimeout(() => jokes.tellJoke(say).catch(logError), 4000) + } +} + +const idFromWord = word => { + if (!word?.startsWith('<@') || !word.endsWith('>')) { + return null + } + return word.substring(2, word.length - 1) +} + +const getSeconds = () => new Date().getTime() / 1000 + +const commas = num => num.toLocaleString() + +const game = loadGame() +const { users, nfts, squad } = game + +const setHighestCoins = userId => { + const prevMax = users[userId].highestEver || 0 + if (prevMax < users[userId].coins) { + users[userId].highestEver = users[userId].coins + } +} + +const addAchievement = (user, achievementName, say) => { + if (!achievements[achievementName]) { + logError(`Achievement ${achievementName} does not exist!`) + return + } + if (user.achievements[achievementName]) { + return + } + setTimeout(async () => { + user.achievements[achievementName] = true + saveGame() + await say(`You earned the achievement ${achievements[achievementName].name}!`) + }, 500) +} + +const getUser = userId => { + if (!users[userId]) { + users[userId] = { + coins: 0, + items: {}, + upgrades: {}, + achievements: {}, + coinsAllTime: 0, + prestige: 0 + } + } else { + users[userId].items ??= {} + users[userId].upgrades ??= {} + users[userId].achievements ??= {} + users[userId].coinsAllTime ??= users[userId].coins + users[userId].prestige ??= 0 + } + return users[userId] +} + +const getCoins = userId => { + const user = getUser(userId) + const currentTime = getSeconds() + const lastCheck = user.lastCheck || currentTime + const secondsPassed = currentTime - lastCheck + + const increase = getCPS(userId) * secondsPassed + user.coins += increase + user.coinsAllTime += increase + user.coins = Math.floor(user.coins) + + user.lastCheck = currentTime + setHighestCoins(userId) + saveGame() + return user.coins +} + +const getCPS = userId => { + const user = getUser(userId) + const userItems = user?.items || {} + return Math.round(Object.keys(userItems).reduce((total, itemName) => total + getItemCps(user, itemName), 0)) +} + +const getItemCps = (user, itemName) => (user.items[itemName] || 0) * singleItemCps(user, itemName) + +const squadUpgrades = { + tastyKeyboards: { + name: 'Tasty Keyboards', + description: 'Delicious and sticky. Boosts CPS by 20% for everyone.', + effect: cps => Math.ceil(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), + cost: 100_000_000_000_000, + emoji: 'printer' + } +} + +const squadHas = ([name]) => squad.upgrades[name] === true +const squadIsMissing = name => !squadHas(name) + +const getCompletedSquadgrades = () => + Object.entries(squadUpgrades) + .filter(squadHas) + .map(([, upgrade]) => upgrade) + +const singleItemCps = (user, itemName) => { + const baseCps = buyableItems[itemName].earning + + const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name]) + const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), baseCps) + + const userGeneralUpgrades = user.upgrades.general || [] + const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), itemUpgradeCps) + + const achievementCount = Object.keys(user.achievements || {}).length + const achievementMultiplier = Math.pow(1.01, achievementCount) + + const userQuackgrades = user.quackUpgrades?.cps || [] + const quackMultiplier = userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1) + + const prestigeMultiplier = 1 + ((user.prestige || 0) * 0.01) + + return achievementMultiplier * + quackMultiplier * + prestigeMultiplier * + getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), generalUpgradeCps) +} + +module.exports = { + saveGame, + logError, + parseOr, + maybeNews, + idFromWord, + commas, + setHighestCoins, + addAchievement, + getCoins, + getUser, + singleItemCps, + getCPS, + getItemCps, + squadUpgrades, + squadIsMissing, + game +} \ No newline at end of file diff --git a/src/games/hvacoins/webapi.js b/src/games/hvacoins/webapi.js new file mode 100644 index 0000000..bb11384 --- /dev/null +++ b/src/games/hvacoins/webapi.js @@ -0,0 +1,69 @@ +const express = require('express') +const app = express() +const port = 3001 +const crypto = require('crypto') +const base64 = require('base-64') +const slack = require('../../slack') +const { game: { users } } = require('./utils') + +const apiGetUserId = hash => { + return Object.entries(userGetter.users) + .filter(([id, user]) => user.pwHash === hash) + .map(([id, user]) => id)[0] +} + +const makeHash = pw => + crypto.createHash('md5') + .update(pw) + .digest('hex') + +const addCommand = ({ commandNames, helpText, action, condition, hidden }) => { + const route = async (req, res) => { + const say = async msg => res.send(msg) + try { + const words = ['', ...Object.keys(req.query)] + console.log('INCOMING API CALL:', name, words) + const encoded = req.header('Authorization').substring(5) + const decoded = base64.decode(encoded).substring(1) + const event = { + user: apiGetUserId(makeHash(decoded)) + } + if (!event.user) { + res.status(400) + res.send( + 'User does not exist, or does not have a password.\n' + + 'See \'!setpw help\' for assistance.' + ) + console.log(' bad password') + return + } + const lastCall = userGetter.users[event.user].lastApiCall || 0 + const secondsBetweenCalls = 5 + const currentTime = Math.floor(new Date().getTime() / 1000) + if (lastCall + secondsBetweenCalls > currentTime) { + res.status(400) + res.send(`Must have at least ${secondsBetweenCalls}s between api calls`) + console.log(' rate limited') + return + } + console.log(` went through for ${slack.ourUsers[event.user]}`) + userGetter.users[event.user].lastApiCall = currentTime + + await action({event, say, words}) + } catch (e) { + console.error(e) + await say(e.stack) + } + } + commandNames.forEach(name => + app.get('/' + name.replace(/!/gi, ''), route) + ) +} + +module.exports = { + addCommand, + makeHash, + launch: () => app.listen(port, () => { + console.log(`Express listening on port ${port}`) + }) +} \ No newline at end of file diff --git a/src/games/index.js b/src/games/index.js index 803dca8..f4abadc 100644 --- a/src/games/index.js +++ b/src/games/index.js @@ -1,4 +1,5 @@ require('./connect4') require('./tictactoe') require('./jokes') -require('./hvacoins') \ No newline at end of file +require('./hvacoins') +require('./trivia') diff --git a/src/games/jokes.js b/src/games/jokes.js index cec73e6..47e198d 100644 --- a/src/games/jokes.js +++ b/src/games/jokes.js @@ -2,107 +2,107 @@ const slack = require('../slack') // TODO: Move jokes/news into their own files, and let hvacker edit them when !addjoke or !addnews are used const jokes = [ - ['What do you call a duck that steals things from the bathroom?', 'A robber ducky.'], - ['On what side does a duck have the most feathers?', 'The outside.'], - ['Why did the duck cross the playground?', 'To get to the other slide.'], - ['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'], - ['Why do ducks lay eggs?', 'They would break if they dropped them.'], - ['Why do ducks quack?', 'Well, because they can\'t oink, or moo, or bark.'], - ['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'], - ['Why did the duck cross the road?', 'To show the chicken how to do it.'], - // Puns: - ['Why do ducks make good detectives?', 'Because they always quack the case!'], - ['When does a duck get up in the morning?', 'At the quack of dawn!'], - ['What do you call a duck that loves fireworks?', 'A fire-quacker.'], - ['What did the duck say to the waiter?', '"Put it on my bill."'], - ['Where do sick ducks go?', 'To the Ductor!'], - ['What kind of TV shows do ducks watch?', 'Duckumenteries!'], - ['What type of food do you get when you cross a duck with a mole?', 'Duckamole!'], - ['What did the duck say when he dropped the dishes?', '"I hope I didn\'t quack any!"'], - ['What is a duck\'s favourite game?', '"Beak-a-boo!"'], - ['Why did the duck cross the road?', 'Because there was a quack in the pavement!'], - ['What has webbed feet and fangs?', 'Count Duckula!'], - ['What do ducks get after they eat?', 'A bill.'], - ['What do ducks eat with their soup?', 'Quackers.'], - ['What happens when you say something funny to a duck?', 'It quacks up.'], - ['What\'s a duck\'s favourite ballet?', 'The Nutquacker.'], - ['What do ducks say when people throw things at them?', '"Time to duck!"'], - ['Why are ducks so good at fixing things?', 'Because they\'re great at using duck-tape!'], - ['What do you get when you put a bunch of rubber ducks in a box?', 'A box of quackers.'], - ['Why was the teacher annoyed with the duck?', 'Because it wouldn\'t stop quacking jokes!'], - ['What did the duck eat for a snack?', 'Salted quackers!'], - ['What do you call a rude duck?', 'A duck with a quackitude.'], - ['What did the lawyer say to the duck in court?', '"I demand an egg-splanation!"'], - ['How can you tell rubber ducks apart?', 'You can\'t, they look egg-xactly the same!'], - ['Why are ducks good at budgeting?', 'They know how to handle the bills!'], - ['Where do tough ducks come from?', 'Hard-boiled eggs.'], - ['Why do ducks have webbed feet?', 'To stomp out fires.', 'Why do elephants have big feet?', 'To stomp out burning ducks.'], - ['Why do ducks check the news?', 'For the feather forecast.'], - ['What did the ducks carry their schoolbooks in?', 'Their quack-packs.'], - ['What do you call it when it\'s raining ducks and chickens?', 'Fowl weather.'], - ['Why did the duck get a red card in the football game?', 'For Fowl-play.'], - ['What did the duck say to the spider?', '"Why don\'t you have webbed feet?"'], - ['What do you get if you cross a duck with an accountant?', 'A bill with a bill.'], - ['What do you call a duck\'s burp?', 'A fowl smell!'], - ['What do you get if you cross a vampire, duck and a sheep?', 'Count Duck-ewe-la.'], - ['What do you call a duck that\'s very clever?', 'A wise-quacker.'], - ['Why do ducks never ask for directions?', 'They prefer to wing it.'], - // I wrote dis - ['What did the man say to his wife when a duck flew at her head?', '"Look out! There\'s a duck flying at your head!"'], - ['What kind of duck plays goalie?', 'A hockey duck.'], - ['What kind of bird gets a job here?', 'A software duckveloper!'], - ['How many ducks does it take to screw in a light bulb?', 'Ducks do not live indoors.'], - ['What kind of drug does a duck like?', 'Quack.'] + ['What do you call a duck that steals things from the bathroom?', 'A robber ducky.'], + ['On what side does a duck have the most feathers?', 'The outside.'], + ['Why did the duck cross the playground?', 'To get to the other slide.'], + ['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'], + ['Why do ducks lay eggs?', 'They would break if they dropped them.'], + ['Why do ducks quack?', 'Well, because they can\'t oink, or moo, or bark.'], + ['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'], + ['Why did the duck cross the road?', 'To show the chicken how to do it.'], + // Puns: + ['Why do ducks make good detectives?', 'Because they always quack the case!'], + ['When does a duck get up in the morning?', 'At the quack of dawn!'], + ['What do you call a duck that loves fireworks?', 'A fire-quacker.'], + ['What did the duck say to the waiter?', '"Put it on my bill."'], + ['Where do sick ducks go?', 'To the Ductor!'], + ['What kind of TV shows do ducks watch?', 'Duckumenteries!'], + ['What type of food do you get when you cross a duck with a mole?', 'Duckamole!'], + ['What did the duck say when he dropped the dishes?', '"I hope I didn\'t quack any!"'], + ['What is a duck\'s favourite game?', '"Beak-a-boo!"'], + ['Why did the duck cross the road?', 'Because there was a quack in the pavement!'], + ['What has webbed feet and fangs?', 'Count Duckula!'], + ['What do ducks get after they eat?', 'A bill.'], + ['What do ducks eat with their soup?', 'Quackers.'], + ['What happens when you say something funny to a duck?', 'It quacks up.'], + ['What\'s a duck\'s favourite ballet?', 'The Nutquacker.'], + ['What do ducks say when people throw things at them?', '"Time to duck!"'], + ['Why are ducks so good at fixing things?', 'Because they\'re great at using duck-tape!'], + ['What do you get when you put a bunch of rubber ducks in a box?', 'A box of quackers.'], + ['Why was the teacher annoyed with the duck?', 'Because it wouldn\'t stop quacking jokes!'], + ['What did the duck eat for a snack?', 'Salted quackers!'], + ['What do you call a rude duck?', 'A duck with a quackitude.'], + ['What did the lawyer say to the duck in court?', '"I demand an egg-splanation!"'], + ['How can you tell rubber ducks apart?', 'You can\'t, they look egg-xactly the same!'], + ['Why are ducks good at budgeting?', 'They know how to handle the bills!'], + ['Where do tough ducks come from?', 'Hard-boiled eggs.'], + ['Why do ducks have webbed feet?', 'To stomp out fires.', 'Why do elephants have big feet?', 'To stomp out burning ducks.'], + ['Why do ducks check the news?', 'For the feather forecast.'], + ['What did the ducks carry their schoolbooks in?', 'Their quack-packs.'], + ['What do you call it when it\'s raining ducks and chickens?', 'Fowl weather.'], + ['Why did the duck get a red card in the football game?', 'For Fowl-play.'], + ['What did the duck say to the spider?', '"Why don\'t you have webbed feet?"'], + ['What do you get if you cross a duck with an accountant?', 'A bill with a bill.'], + ['What do you call a duck\'s burp?', 'A fowl smell!'], + ['What do you get if you cross a vampire, duck and a sheep?', 'Count Duck-ewe-la.'], + ['What do you call a duck that\'s very clever?', 'A wise-quacker.'], + ['Why do ducks never ask for directions?', 'They prefer to wing it.'], + // I wrote dis + ['What did the man say to his wife when a duck flew at her head?', '"Look out! There\'s a duck flying at your head!"'], + ['What kind of duck plays goalie?', 'A hockey duck.'], + ['What kind of bird gets a job here?', 'A software duckveloper!'], + ['How many ducks does it take to screw in a light bulb?', 'Ducks do not live indoors.'], + ['What kind of drug does a duck like?', 'Quack.'] ] const news = [ - 'Duck criminal escapes from duck prison. Whereabouts unknown.', - 'Criminal mastermind duck believed to have taken refuge on Slack.', - 'Infamous _"quackers"_ NFT may be related to recent Chicago crime spree. More at 11.', - 'Six geese arrested under suspicion of honking.', - 'Swan under investigation for illegal trumpet-smuggling ring.', - 'Local rooster the subject of serious egg-stealing allegations.', - 'Inducknesian court rules that the news is certainly _not_ controlled by an all-powerful duck lord, and that everyone should please stop asking about it.', - '"Spending 6,000,000,000 HVAC on a space yacht was the best decision of my life", reveals local billionaire/jerk.', - '16 Unethical Egg Hacks You Won\'t Learn In School. You can\'t believe number 5!', - 'The word "Tuesday" declared illegal. Refer to it as "Quackday" from now on.', - 'Has anyone been killed/maimed/publicly humiliated for not following the duck lord\'s orders? The answer might surprise you.', - '_They called him crazy -_ Local duck bores to the center of the earth and lives with the mole people.', - 'Jerry Seinfeld - Love affair with a hen! Is this the new Hollywood power couple?', - 'Is your uncle secretly a goose? There\'s simply no way of knowing.', - 'Danny Duckvito considers opening his own chain of supermarkets. He is quoted as saying "Heyyyy, I\'m Danny Duckvito"', - 'Slack-related-gambling epidemic! People around the globe are betting their digital lives away.' + 'Duck criminal escapes from duck prison. Whereabouts unknown.', + 'Criminal mastermind duck believed to have taken refuge on Slack.', + 'Infamous _"quackers"_ NFT may be related to recent Chicago crime spree. More at 11.', + 'Six geese arrested under suspicion of honking.', + 'Swan under investigation for illegal trumpet-smuggling ring.', + 'Local rooster the subject of serious egg-stealing allegations.', + 'Inducknesian court rules that the news is certainly _not_ controlled by an all-powerful duck lord, and that everyone should please stop asking about it.', + '"Spending 6,000,000,000 HVAC on a space yacht was the best decision of my life", reveals local billionaire/jerk.', + '16 Unethical Egg Hacks You Won\'t Learn In School. You can\'t believe number 5!', + 'The word "Tuesday" declared illegal. Refer to it as "Quackday" from now on.', + 'Has anyone been killed/maimed/publicly humiliated for not following the duck lord\'s orders? The answer might surprise you.', + '_They called him crazy -_ Local duck bores to the center of the earth and lives with the mole people.', + 'Jerry Seinfeld - Love affair with a hen! Is this the new Hollywood power couple?', + 'Is your uncle secretly a goose? There\'s simply no way of knowing.', + 'Danny Duckvito considers opening his own chain of supermarkets. He is quoted as saying "Heyyyy, I\'m Danny Duckvito"', + 'Slack-related-gambling epidemic! People around the globe are betting their digital lives away.' ] const tellJoke = async say => { - const joke = jokes[Math.floor(Math.random() * jokes.length)] + const joke = jokes[Math.floor(Math.random() * jokes.length)] - let timeout = 0 - for (const part of joke) { - setTimeout(async () => await say(part), timeout); - timeout += 5000 - } + let timeout = 0 + for (const part of joke) { + setTimeout(async () => say(part), timeout) + timeout += 5000 + } } const newsAlert = async say => { - let article = lastNews - while(article === lastNews) { - article = Math.floor(Math.random() * news.length) - } - lastNews = article - await say(news[article]) + let article = lastNews + while (article === lastNews) { + article = Math.floor(Math.random() * news.length) + } + lastNews = article + await say(news[article]) } let lastNews = -1 slack.onMessage(async ({ event, say }) => { if (event.text?.toLowerCase() === '!joke') { - await tellJoke(say) + await tellJoke(say) } else if (event.text?.toLowerCase() === '!news') { - await newsAlert(say) + await newsAlert(say) } }) module.exports = { - tellJoke, - newsAlert, -} \ No newline at end of file + tellJoke, + newsAlert +} diff --git a/src/games/routine.js b/src/games/routine.js index 5f124c5..881aeac 100644 --- a/src/games/routine.js +++ b/src/games/routine.js @@ -3,16 +3,16 @@ const slack = require('../slack') const tie = 'TIE' const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) => - gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' + - '```' + textFromBoard(board) + '\n```' + gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' + + '```' + textFromBoard(board) + '\n```' const addChoiceEmojis = async ({ choices, channel, ts }) => { const addEmoji = async emojiName => - await slack.app.client.reactions.add({ - channel, - timestamp: ts, - name: emojiName - }) + await slack.app.client.reactions.add({ + channel, + timestamp: ts, + name: emojiName + }) for (const choice of choices) { await addEmoji(choice) } @@ -32,12 +32,12 @@ const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, in player2: opponent }) const sent = await say(msg) - await addChoiceEmojis({...sent, choices: turnChoiceEmojis}) + await addChoiceEmojis({ ...sent, choices: turnChoiceEmojis }) } } } -const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, {board, players}) +const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, { board, players }) const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message) @@ -95,33 +95,33 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn } const removeEmoji = async emojiName => - slack.app.client.reactions.remove({ - channel: event.item.channel, - timestamp: message.messages[0]?.ts, - name: emojiName - }) + slack.app.client.reactions.remove({ + channel: event.item.channel, + timestamp: message.messages[0]?.ts, + name: emojiName + }) turnChoiceEmojis.forEach(removeEmoji) const sentBoard = await slack.app.client.chat.postMessage({ channel: opponent, text: boardMessage + winnerMessages.opponent }) if (!winner) { - await addChoiceEmojis({...sentBoard, choices: turnChoiceEmojis}) + await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis }) } } module.exports = { tie, build: ({ - startTriggers, - initialBoard, - turnChoiceEmojis, - gameName, - textFromBoard, - checkWinner, - makeMove, - }) => { - const dataName = gameName.replace(/[^0-9a-zA-Z]/gi, '') + startTriggers, + initialBoard, + turnChoiceEmojis, + gameName, + textFromBoard, + checkWinner, + makeMove + }) => { + const dataName = gameName.replace(/[^0-9A-Z]/gi, '') const gameStarter = buildGameStarter({ startTriggers, gameName, @@ -129,7 +129,7 @@ module.exports = { initialBoard, textFromBoard, turnChoiceEmojis - }); + }) slack.onMessage(gameStarter) const turnHandler = buildTurnHandler({ diff --git a/src/games/trivia.js b/src/games/trivia.js new file mode 100644 index 0000000..fc964a5 --- /dev/null +++ b/src/games/trivia.js @@ -0,0 +1,13 @@ +const axios = require('axios') + +const getTrivia = async () => axios.get('https://opentdb.com/api.php?amount=10&category=9&difficulty=medium&type=multiple', { + headers: { + Accept: 'application/json, text/plain, */*' + } +}) + .then(res => res.data.results) + .catch(console.error) + +module.exports = { + getTrivia +} diff --git a/src/honeywell/index.js b/src/honeywell/index.js index c9b5212..24c7a08 100644 --- a/src/honeywell/index.js +++ b/src/honeywell/index.js @@ -1,6 +1,6 @@ const postToHoneywell = options => {} -const postTemp = ({ heatSetpoint, coolSetpoint, mode}) => { +const postTemp = ({ heatSetpoint, coolSetpoint, mode }) => { postToHoneywell({ heatSetpoint, coolSetpoint, diff --git a/src/slack/index.js b/src/slack/index.js index 1693af7..734cc40 100644 --- a/src/slack/index.js +++ b/src/slack/index.js @@ -5,7 +5,7 @@ const temperatureChannelId = 'C034156CE03' const hvackerBotUserId = 'U0344TFA7HQ' const sageUserId = 'U028BMEBWBV' -const pollingMinutes = 10 +const pollingMinutes = 5 const pollingPeriod = 1000 * 60 * pollingMinutes const colderEmoji = 'snowflake' @@ -14,175 +14,200 @@ const goodEmoji = '+1' let app try { - app = new SlackApp({ - token: config.slackBotToken, - signingSecret: config.slackSigningSecret, - appToken: config.slackAppToken, - socketMode: true - }) - // app.client.conversations.list({types: 'private_channel'}).then(fetched => { - // temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id - // console.log('techThermostatChannelId', temperatureChannelId) - // }) + app = new SlackApp({ + token: config.slackBotToken, + signingSecret: config.slackSigningSecret, + appToken: config.slackAppToken, + socketMode: true + }) + // app.client.conversations.list({types: 'private_channel'}).then(fetched => { + // temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id + // console.log('techThermostatChannelId', temperatureChannelId) + // }) } catch (e) { - console.log('Failed to initialize SlackApp', e) + console.log('Failed to initialize SlackApp', e) } const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold'] const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>'] const sendHelp = async (say, prefix) => { - if (prefix) { - prefix = prefix + '\n' - } else { - prefix = '' - } + if (prefix) { + prefix = prefix + '\n' + } else { + prefix = '' + } - await say({ - text: prefix + - `Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` + - 'At this time I am not capable of actually changing the temperature. Go bug Quade.' - }) + await say({ + text: prefix + + `Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` + + 'At this time I am not capable of actually changing the temperature. Go bug Quade.' + }) } -const getMessage = async ({channel, ts}) => app.client.conversations.history({ - channel: channel, - latest: ts, - inclusive: true, - limit: 1 +const getMessage = async ({ channel, ts }) => app.client.conversations.history({ + channel: channel, + latest: ts, + inclusive: true, + limit: 1 }) app.event('reaction_added', async ({ event, context, client, say }) => { - for (const listener of reactionListeners) { - listener({ event, say }) - } + for (const listener of reactionListeners) { + listener({ event, say }) + } }) const ourUsers = { - 'U028BMEBWBV': 'Sage', - 'U02U15RFK4Y': 'Adam', - 'U02AAB54V34': 'Houston', - 'U02KYLVK1GV': 'Quade', - 'U017PG4EL1Y': 'Max', - 'UTDLFGZA5': 'Tyler' + U028BMEBWBV: 'Sage', + U02U15RFK4Y: 'Adam', + U02AAB54V34: 'Houston', + U02KYLVK1GV: 'Quade', + U017PG4EL1Y: 'Max', + UTDLFGZA5: 'Tyler', + U017CB5L1K3: 'Andres' } -let activePolls = {} +const activePolls = {} +const testId = 'U028BMEBWBV_TEST' +let testMode = false app.event('message', async ({ event, context, client, say }) => { - for (const listener of messageListeners) { - listener({ event, say }) + console.log(event) + if (event.user === sageUserId) { + if (event?.text.startsWith('!')) { + if (testMode) { + await messageSage('Currently in test mode!') + } } - console.log('MSG', ourUsers[event.user], "'" + event.text + "'", new Date().toLocaleTimeString()) - if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') { - if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) { - 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 - } + if (event?.text === '!test') { + testMode = !testMode + await messageSage(`TestMode: ${testMode} with ID ${testId}`) + } else if (event?.text === '!notest') { + testMode = false + await messageSage(`TestMode: ${testMode}`) } - let eventText = event.text?.toLowerCase() || '' + 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.text === '!!kill') { + process.exit() + } + if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) { + 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')) { - await sendHelp(say) - return + if (eventText.startsWith('!help')) { + await sendHelp(say) + return + } + + if (!pollTriggers.includes(eventText)) { + if (halfTriggers.includes(eventText)) { + await sendHelp(say, 'It looks like you might want to change the temperature.') + } + return + } + + if (activePolls[event.channel]) { + await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" }) + return + } + + activePolls[event.channel] = true + const pollTs = await startPoll() + setTimeout(async () => { + const reactions = await app.client.reactions.get({ + channel: temperatureChannelId, + timestamp: pollTs, + full: true + }) + const reactCounts = {} + reactions.message.reactions.forEach(reaction => { reactCounts[reaction.name] = reaction.count }) + + const contentVotes = reactCounts[goodEmoji] + const hotterVotes = reactCounts[hotterEmoji] + const colderVotes = reactCounts[colderEmoji] + + let text = 'The people have spoken, and would like to ' + if (hotterVotes > colderVotes && hotterVotes > contentVotes) { + text += 'raise the temperature, quack.' + requestTempChange('Hotter') + } else if (colderVotes > hotterVotes && colderVotes > contentVotes) { + text += 'lower the temperature, quack quack.' + requestTempChange('Colder') + } else { + text += 'keep the temperature as-is, quaaack.' + requestTempChange('Good') } - if (!pollTriggers.includes(eventText)) { - if (halfTriggers.includes(eventText)) { - await sendHelp(say, 'It looks like you might want to change the temperature.') - } - return - } - - if (activePolls[event.channel]) { - await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" }) - return - } - - activePolls[event.channel] = true - const pollTs = await startPoll() - setTimeout(async () => { - const reactions = await app.client.reactions.get({ - channel: temperatureChannelId, - timestamp: pollTs, - full: true - }) - const reactCounts = {} - reactions.message.reactions.forEach(reaction => reactCounts[reaction.name] = reaction.count) - - const contentVotes = reactCounts[goodEmoji] - const hotterVotes = reactCounts[hotterEmoji] - const colderVotes = reactCounts[colderEmoji] - - let text = 'The people have spoken, and would like to ' - if (hotterVotes > colderVotes && hotterVotes > contentVotes) { - text += 'raise the temperature, quack.' - requestTempChange('Hotter') - } else if (colderVotes > hotterVotes && colderVotes > contentVotes) { - text += 'lower the temperature, quack quack.' - requestTempChange('Colder') - } else { - text += 'keep the temperature as-is, quaaack.' - requestTempChange('Good') - } - - await postToTechThermostatChannel({ text }) - delete activePolls[event.channel] - }, pollingPeriod) + await postToTechThermostatChannel({ text }) + delete activePolls[event.channel] + }, pollingPeriod) }) ;(async () => { - await app.start().catch(console.error) - console.log('Slack Bolt has started') - //setTimeout(async () => { - // await messageSage('') - //}, 2000) -})(); + await app.start().catch(console.error) + console.log('Slack Bolt has started') + // setTimeout(async () => { + // await messageSage('') + // }, 2000) +})() const postToTechThermostatChannel = async optionsOrText => { - if (optionsOrText === null || typeof optionsOrText !== 'object') { - optionsOrText = { - text: optionsOrText - } + if (optionsOrText === null || typeof optionsOrText !== 'object') { + optionsOrText = { + text: optionsOrText } - return await app.client.chat.postMessage({...optionsOrText, channel: temperatureChannelId}) + } + return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId }) } const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText) const messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText) const messageIn = async (channel, optionsOrText) => { - if (optionsOrText === null || typeof optionsOrText !== 'object') { - optionsOrText = { - text: optionsOrText - } + if (optionsOrText === null || typeof optionsOrText !== 'object') { + optionsOrText = { + text: optionsOrText } - return await app.client.chat.postMessage({...optionsOrText, channel}) + } + return app.client.chat.postMessage({ ...optionsOrText, channel }) } const startPoll = async () => { - const sent = await postToTechThermostatChannel({ - text: ` Temperature poll requested! In ${pollingMinutes} minute(s) 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!)` + 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!)` + }) + const addReaction = async emojiName => + app.client.reactions.add({ + channel: temperatureChannelId, + timestamp: sent.ts, + name: emojiName }) - const addReaction = async emojiName => - await app.client.reactions.add({ - channel: temperatureChannelId, - timestamp: sent.ts, - name: emojiName - }) - await addReaction(colderEmoji) - await addReaction(hotterEmoji) - await addReaction(goodEmoji) - return sent.ts + await addReaction(colderEmoji) + await addReaction(hotterEmoji) + await addReaction(goodEmoji) + return sent.ts } const tempChangeListeners = [] @@ -190,45 +215,52 @@ const messageListeners = [] const reactionListeners = [] const requestTempChange = change => { - tempChangeListeners.forEach(listener => listener(change)) + tempChangeListeners.forEach(listener => listener(change)) } const encodeData = (key, data) => - `` + `` const decodeData = (key, message) => { - const regex = new RegExp(`http://${key}ZZZ[^|]*`) - let match = message.match(regex) - if (!match) { - return match - } - match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length - return JSON.parse(Buffer.from(match, 'base64').toString('utf-8')) + const regex = new RegExp(`http://${key}ZZZ[^|]*`) + let match = message.match(regex) + if (!match) { + return match + } + match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length + return JSON.parse(Buffer.from(match, 'base64').toString('utf-8')) } const onReaction = listener => reactionListeners.push(listener) -onReaction(async ({ event, say }) => { - if (event.user === sageUserId && event.reaction === 'x') { - console.log(event) - await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts }) +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) } + } }) module.exports = { - app, - hvackerBotUserId, - temperatureChannelId, - onAction: app.action, - getMessage, - updateMessage: app.client.chat.update, - postToTechThermostatChannel, - onTempChangeRequested: listener => tempChangeListeners.push(listener), - onMessage: listener => messageListeners.push(listener), - onReaction, - encodeData, - decodeData, - sageUserId, - messageSage, - ourUsers -} \ No newline at end of file + app, + hvackerBotUserId, + temperatureChannelId, + onAction: app.action, + getMessage, + updateMessage: app.client.chat.update, + postToTechThermostatChannel, + onTempChangeRequested: listener => tempChangeListeners.push(listener), + onMessage: listener => messageListeners.push(listener), + onReaction, + encodeData, + decodeData, + sageUserId, + messageSage, + messageIn, + testMode, + testId, + ourUsers +}