const { Pool } = require('pg') const fs = require('fs') const config = require('../../config') const achievements = require('./achievements') const buyableItems = require('./buyableItems') const { quackStore, getChaos } = require('./quackstore') const dbPool = new Pool(config.postgres) let jokes let slackUsers const setSlackUsers = users => { slackUsers = users } let upgrades const setUpgrades = upg => { upgrades = upg } const saveFile = 'hvacoins.json' const saveDir = '/hvacker-saves/' const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ } const loadGame = () => { const game = parseOr(fs.readFileSync(saveDir + saveFile, 'utf-8'), () => ({ users: {}, nfts: [], squad: {}, horrors: {} })) game.horrors ??= {} return game } const chaosFilter = (num, odds, user, max = Infinity, min = -Infinity) => { const userQuackgrades = user.quackUpgrades?.cps || [] const hasChaos = userQuackgrades.includes('chaos') if (!hasChaos || Math.random() < odds || !num) { return num } const chaosed = num * getChaos(user) if (chaosed > max) { return max } if (chaosed < min) { return min } return chaosed } const parseOr = (parseable, fallback) => { try { if (typeof parseable === 'function') { parseable = parseable() } return JSON.parse(parseable) } catch (e) { logError(e) return fallback() } } let lastBackupTs = 0 const makeBackup = force => { const currentTs = Date.now() if (lastBackupTs > (currentTs - 60000) && !force) { return } lastBackupTs = currentTs const cleanNowString = new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_') const fileName = `${saveDir}backups/${cleanNowString}-${saveFile}` console.log(`Making backup file: ${fileName}`) fs.writeFileSync(fileName, JSON.stringify(game)) } const saveUser = async (userId, user, after) => { const name = user.name || userId if (after) { console.log(`SAVING ${name} after ${after}`) } else { console.log(`SAVING ${name}`, user) } return await dbPool.query(` INSERT INTO hvacker_user (slack_id, name, data) VALUES ($1, $2, $3) ON CONFLICT (slack_id) DO UPDATE SET data = EXCLUDED.data` , [userId, user.name, user]) .catch(console.error) } const saveAllUsers = () => Promise.all( Object.entries(game.users).map(async ([userId, user]) => await saveUser(userId, user) ) ).then(() => { console.log('All users updated in the DB') }) let saves = 0 const saveGame = (after, force = true, skipLog = false) => { if (saves % 20 === 0) { makeBackup() saveAllUsers().catch(console.error) } saves += 1 if (force || saves % 10 === 0) { if (!skipLog) { if (after) { console.log(`SAVING GAME after ${after}`) } else { console.log('SAVING GAME') } } fs.writeFileSync(saveDir + 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 word.replace(/<#([^|]*)|.*/g, '$1') } if (!word?.startsWith('<@') || !word.endsWith('>')) { return getIdFromName(word) } else { return word.substring(2, word.length - 1) } } const getSeconds = () => new Date().getTime() / 1000 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], ['septillion', 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], ['qt', 1_000_000_000_000_000_000], ['qd', 1_000_000_000_000_000], ['tr', 1_000_000_000_000], ['b', 1_000_000_000], ['m', 1_000_000], ] const commas = (num, precise = false, skipWords = false) => { num = Math.round(num) if (num === 1) { return 'one' } const bigNum = bigNumberWords.find(([, base]) => num >= base) if (bigNum && !precise) { const [name, base] = bigNum const nummed = (num / base).toPrecision(3) if (skipWords) { return nummed } return `${nummed} ${name}` } return num.toLocaleString() } const parseAll = (str, allNum, user) => { if (!str) { return NaN } str = str?.toLowerCase()?.replace(/,/g, '') || '1' switch (str) { case 'all': case 'all in': case 'everything': case 'sugma': case 'ligma': case 'pulma': case 'deez': case 'max_int': case 'my soul': return allNum case 'sex': case 'sex number': case 'nice': 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 } if (user && buyableItems[str]) { return calculateCost({ itemName: str, user, quantity: 1 }) } console.log('STR', str) if (str.match(/^\d+$/)) { return parseInt(str) } if (allNum && str.match(/^some$/)) { return Math.floor(Math.random() * allNum) } if (allNum && str.match(/^\d+%$/)) { const percent = parseFloat(str) / 100 if (percent > 1 || percent < 0) { return NaN } return Math.round(percent * allNum) } if (str.match(/^\d+\.\d+$/)) { 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 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 game = loadGame() let { users, nfts, squad } = game const getAllUsers = async () => { const result = await dbPool.query(` SELECT slack_id, data FROM hvacker_user `).catch(console.error) return Object.fromEntries(result.rows.map( ({ slack_id: slackId, data }) => [slackId, data])) } // getAllUsers().then(collection => { // game.users = (users = collection) // }) 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(`${user.name} earned ${achievementName}`) await say(`You earned the achievement ${achievements[achievementName].name}!`) }, 500) } const fuzzyMatcher = string => new RegExp((string?.toLowerCase() || '').split('').join('.*'), 'i') let knownUsers = {} const getIdFromName = name => { const matcher = fuzzyMatcher(name?.toLowerCase()) const found = Object.entries(knownUsers).find(([id, knownName]) => matcher.test(knownName?.toLowerCase())) if (found) { return found[0] } return null; } const fetchUser = async (userId, updateCoins = false) => { const result = await dbPool.query(` SELECT data FROM hvacker_user WHERE slack_id = $1` , [userId]) .catch(console.error) return result.rows[0]?.data } const getUser = async (userId, updateCoins = false) => { // users[userId] = await fetchUser(userId) // console.log('USER', users[userId]) //users[userId] = await fetchUser(userId) return getUserSync(userId, updateCoins) } const getUserSync = (userId, updateCoins = false) => { users[userId] ??= {} users[userId].coins ??= 0 users[userId].items ??= {} users[userId].upgrades ??= {} users[userId].achievements ??= {} users[userId].coinsAllTime ??= users[userId].coins users[userId].prestige ??= 0 users[userId].startDate ??= new Date() // users[userId].name ??= slack.users[userId] if (updateCoins) { users[userId].coins = getCoins(userId, users[userId]) } saveGame('getUserSync()', true, true) return users[userId] } const addCoins = (user, add) => { user.coins += add user.coinsAllTime += add user.coinsAllTime = Math.floor(user.coinsAllTime) user.coins = Math.floor(user.coins) } const getCoins = (userId, user) => { user = user || getUserSync(userId) const currentTime = getSeconds() const lastCheck = user.lastCheck || currentTime const secondsPassed = currentTime - lastCheck addCoins(user, getCPS(user) * secondsPassed) user.lastCheck = currentTime setHighestCoins(userId) //saveGame() return user.coins } const getCPS = user => { 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 => 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 => cps * 1.2, cost: 100_000_000_000_000, emoji: 'printer' }, discardHumanMorals: { name: 'Neglect human decency', description: `Unlocks a new tier of upgrades, but at what cost?`, effect: cps => cps * 1.1, cost: 100_000_000_000_000_000, emoji: 'hole' }, redemption: { name: 'Redemption', description: 'Can you return from the depths of depravity and save your soul?', effect: cps => cps * 1.1, cost: 1_000_000_000_000_000_000, emoji: 'people_hugging' } } const squadHas = ([name]) => squad.upgrades[name] === true const squadIsMissing = name => !squadHas(name) const getCompletedSquadgrades = () => Object.entries(squadUpgrades) .filter(squadHas) .map(([, upgrade]) => upgrade) const getCompletedSquadgradeNames = () => Object.entries(squadUpgrades) .filter(squadHas) .map(([name]) => name) 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 petQuackGradeMultiplier = user => { const userQuackgrades = user.quackUpgrades?.pet || [] return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), petBoost()) } 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), 1) // console.log('itemUpgradeCps', itemUpgradeCps) user.upgrades.general ??= [] const userGeneralUpgrades = user.upgrades.general 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 quackGrade = quackGradeMultiplier(user) // console.log('quackgrade', quackGrade) const pMult = prestigeMultiplier(user) // console.log('prestigeMultiplier', pMult) const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1) // console.log('squadGradeMultiplier', squadGradeMultiplier) const petMultiplier = petQuackGradeMultiplier(user) //console.log('petMultiplier', petMultiplier) const total = baseCps * achievementMultiplier * itemUpgradeCps * generalUpgradeCps * quackGrade * pMult * squadGradeMultiplier * petMultiplier // 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 || str.length === 1) { return str } if (!percentOdds) { percentOdds = 0.01 } let shuffled = str while (shuffled === str) { shuffled = shufflePercent(str, percentOdds) console.log('Shuffling... "' + shuffled + '"') } 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) } } } const removeReactions = async ({ app, channelId, timestamp, reactions }) => { for (const reaction of reactions) { try { await app.client.reactions.remove({ channel: channelId, timestamp, name: reaction }) } catch (e) { logError(e) } } } const daysSinceEpoch = () => { const today = new Date().getTime() const epoch = new Date(0).getTime() return Math.floor((today - epoch) / (1000 * 60 * 60 * 24)) } 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) } game.stonkMarket ??= { lastDay: daysSinceEpoch(), stonks: { duk: { pattern: "duk", index: 0, price: 1_410_911_983_728 }, quak: { pattern: "quak", index: 0, price: 5_111_242_778_696 }, honk: { pattern: "honk", index: 0, price: 511_915_144_009 }, } } const userHasCheckedQuackgrade = (user, quackGrade) => (user.quackUpgrades?.checked || []).includes(quackGrade) const petBoost = () => { // game.pet ??= makePet() const stats = Object.values(game.pet) const hasTerribleStat = stats.filter(value => value < 1).length > 0 const averageStat = stats.reduce((total, current) => total + current, 0) / stats.length if (hasTerribleStat && averageStat < 3) { return 0.9 } if (averageStat === 10) { return 1.3 } if (!hasTerribleStat && averageStat > 8) { return 1.1 } return 1 } game.channelMaps ??= {} let slackAppClientChatUpdate /** * * @param name String name for this channel map * @param text String of to send. Passed into slack.app.client.chat.update * @param blocks Slack blocks object to send. Passed into slack.app.client.chat.update * @param channel An (optional) new channel to add to the given map * @param ts The timestamp of the message in the new channel to update */ const updateAll = async ({ name, text, blocks, add: { channel, ts } = {} }) => { const channelMap = (game.channelMaps[name] ??= {}) // if (channel && ts && !channelMap[channel]) { // } if (channel && ts) { channelMap[channel] = ts console.log({ channelMap }) } if (text || blocks) { await Promise.all(Object.entries(channelMap).map(async ([channel, ts]) => slackAppClientChatUpdate({ channel, ts, text, blocks }).catch(e => { console.error(e) if (e.toString().includes('message_not_found')) { delete channelMap[channel] saveGame(`removing message ${channel}::${ts} from the ${name} list`) } }) )) } // // const alreadyHas = !!channelMap[channel] // if (channel && ts) { // channelMap[channel] = ts // console.log({ channelMap }) // } // // return alreadyHas } const logMemoryUsage = name => { const formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`; const memoryData = process.memoryUsage(); const formattedData = { rss: `${formatMemoryUsage(memoryData.rss)} -> Resident Set Size - total memory allocated for the process execution`, // heapTotal: `${formatMemoryUsage(memoryData.heapTotal)} -> total size of the allocated heap`, // heapUsed: `${formatMemoryUsage(memoryData.heapUsed)} -> actual memory used during the execution`, // external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`, } if (name) { console.log(name, formattedData) } else { console.log(formattedData) } } module.exports = { saveGame, saveUser, makeBackup, logError, parseOr, maybeNews, idFromWord, commas, setHighestCoins, addAchievement, getCoins, getUser, getUserSync, singleItemCps, getCPS, getItemCps, squadUpgrades, squadIsMissing, prestigeMultiplier, quackGradeMultiplier, shufflePercent, definitelyShuffle, parseAll, getRandomFromArray, chaosFilter, addReactions, removeReactions, getCompletedSquadgradeNames, game, dayOfYear, daysSinceEpoch, userHasCheckedQuackgrade, fuzzyMatcher, addCoins, calculateCost, setKnownUsers: users => knownUsers = users, petBoost, updateAll, setSlackAppClientChatUpdate: update => slackAppClientChatUpdate = update, setUpgrades, setSlackUsers, setJokes: _jokes => jokes = _jokes, logMemoryUsage }