Hvacker/src/games/hvacoins/utils.js

713 lines
19 KiB
JavaScript

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<void>}
*/
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
}