Add Governments.

New betting achievement.
Add !pet
Move from Sage to Admin naming.
Add cursed pics for hauntings (and reduce haunting odds)
Add saveGame() reason messages.
Add mining upgrades.
Add semi-live updating leaderboards.
Add thorough emoji validation.
Move user IDs to a separate json file.
More details (like current coin count) in !buy menu.
Limit temperature polls to prevent spam.
Some work on a prestige menu.
Several additional quackgrades.
Change text games to try editing messages live.
Named upgrades.
This commit is contained in:
Sage Vaillancourt 2023-06-29 10:41:55 -04:00
parent ad021cf9a5
commit 4433c19d04
15 changed files with 7123 additions and 363 deletions

5778
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -76,7 +76,7 @@ const checkDiagonals = board => {
const checkFull = board => { const checkFull = board => {
for (const row of board) { for (const row of board) {
for (const col of row) { for (const col of row) {
if (col !== ' ') { if (col === ' ') {
return null return null
} }
} }

View File

@ -44,6 +44,11 @@ module.exports = {
description: 'H I G H R O L L E R', description: 'H I G H R O L L E R',
emoji: '8ball' emoji: '8ball'
}, },
sigmaBets: {
name: 'Make a bet over 100 Quintillion',
description: 'Return to monke',
emoji: 'yeknom'
},
ignited: { ignited: {
name: 'You light my fire, baby', name: 'You light my fire, baby',
description: 'And you pay attention to descriptions!', description: 'And you pay attention to descriptions!',
@ -120,6 +125,11 @@ module.exports = {
description: `I mean... that's basically all of them.`, description: `I mean... that's basically all of them.`,
emoji: 'office' emoji: 'office'
}, },
government100: {
name: 'Run 100 Governments',
description: `I hope you're using them for something good...`,
emoji: 'japanese_castle'
},
weAllNeedHelp: { weAllNeedHelp: {
name: 'View the \'!coin\' help', name: 'View the \'!coin\' help',
@ -159,7 +169,7 @@ module.exports = {
}, },
bookWorm: { bookWorm: {
name: 'Take a peek at the lore', name: 'Take a peek at the lore',
description: 'It\'t gotta be worth your time somehow.', description: 'It\'s gotta be worth your time somehow.',
emoji: 'books' emoji: 'books'
}, },

View File

@ -1,16 +1,8 @@
const buyableItems = require('./buyableItems') const buyableItems = require('./buyableItems')
const { commas, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter, fuzzyMatcher } = require('./utils') const { commas, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter, fuzzyMatcher, calculateCost } = require('./utils')
const slack = require('../../slack') const slack = require('../../slack')
const calculateCost = ({ itemName, user, quantity = 1 }) => { const leaderboardUpdater = {}
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 getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => {
const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost) const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost)
@ -69,16 +61,28 @@ const buildBlock2 = ({ user, itemName, cost, cps }) => ({
] ]
}) })
const buyText2 = (highestCoins, user) => { const buyText2 = (highestCoins, user, extraMessage = '') => {
return ({ return ({
text: buyableText(highestCoins, user), text: (extraMessage && extraMessage + '\n')
blocks: Object.entries(buyableItems) + `You have ${commas(user.coins)} HVAC to spend.\n`
+ buyableText(highestCoins, user),
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: (extraMessage && extraMessage + '\n')
+ `You have ${commas(user.coins)} HVAC to spend.\n`
},
},
...Object.entries(buyableItems)
.filter(([, item]) => canView(item, highestCoins)) .filter(([, item]) => canView(item, highestCoins))
.map(([itemName]) => { .map(([itemName]) => {
const cost = calculateCost({ itemName, user, quantity: 1 }) const cost = calculateCost({ itemName, user, quantity: 1 })
const cps = Math.round(singleItemCps(user, itemName)) const cps = Math.round(singleItemCps(user, itemName))
return ({ user, itemName, cost, cps }) return ({ user, itemName, cost, cps })
}).map(buildBlock) }).map(buildBlock)
]
}) })
} }
@ -136,7 +140,7 @@ const buyRoute = async ({ event, say, args, user }) => {
return say(`Buying ${quantity} ${buyableName} would cost you ${commas(realCost)} HVAC`) return say(`Buying ${quantity} ${buyableName} would cost you ${commas(realCost)} HVAC`)
} }
if (currentCoins < realCost) { if (currentCoins < realCost) {
await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`) await say(`You don't have enough coins! You need ${commas(realCost)}`)
return return
} }
user.coins -= realCost user.coins -= realCost
@ -161,15 +165,20 @@ const buyButton = async ({ body, ack, say, payload }) => {
const user = getUser(event.user) const user = getUser(event.user)
const words = ['', buying, body.actions[0].text] const words = ['', buying, body.actions[0].text]
const [commandName, ...args] = words const [commandName, ...args] = words
let extraMessage = ''
say = async text => extraMessage = text
await buyRoute({ event, say, words, args, commandName, user }) await buyRoute({ event, say, words, args, commandName, user })
const highestCoins = user.highestEver || user.coins || 1 const highestCoins = user.highestEver || user.coins || 1
await slack.app.client.chat.update({ await slack.app.client.chat.update({
channel: body.channel.id, channel: body.channel.id,
ts: body.message.ts, ts: body.message.ts,
...buyText2(highestCoins, user) ...buyText2(highestCoins, user, extraMessage)
}) })
await leaderboardUpdater.updateAllLeaderboards()
} }
Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton)) Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton))
module.exports = buyRoute module.exports = { buyRoute, leaderboardUpdater }

View File

@ -25,14 +25,14 @@ module.exports = {
earning: 260, earning: 260,
emoji: 'train2', emoji: 'train2',
description: 'Efficiently ship your most valuable coins.', description: 'Efficiently ship your most valuable coins.',
own100Achievement: 'fire100', own100Achievement: 'train100',
}, },
fire: { fire: {
baseCost: 1_400_000, baseCost: 1_400_000,
earning: 1_400, earning: 1_400,
emoji: 'fire', emoji: 'fire',
description: 'Return to the roots of HVAC.', description: 'Return to the roots of HVAC.',
own100Achievement: 'train100', own100Achievement: 'fire100',
}, },
boomerang: { boomerang: {
baseCost: 20_000_000, baseCost: 20_000_000,
@ -46,7 +46,7 @@ module.exports = {
earning: 44_000, earning: 44_000,
emoji: 'new_moon_with_face', emoji: 'new_moon_with_face',
description: 'Convert dark new-moon energy into HVAC Coins.', description: 'Convert dark new-moon energy into HVAC Coins.',
own100Achievement: 'mirror100', own100Achievement: 'moon100',
}, },
butterfly: { butterfly: {
baseCost: 5_100_000_000, baseCost: 5_100_000_000,
@ -60,41 +60,48 @@ module.exports = {
earning: 1_600_000, earning: 1_600_000,
emoji: 'mirror', emoji: 'mirror',
description: 'Only by gazing inward can you collect enough Coin to influence the thermostat.', description: 'Only by gazing inward can you collect enough Coin to influence the thermostat.',
own100Achievement: 'quade100', own100Achievement: 'mirror100',
}, },
quade: { quade: {
baseCost: 1_000_000_000_000, baseCost: 1_000_000_000_000,
earning: 10_000_000, earning: 10_000_000,
emoji: 'quade', emoji: 'quade',
description: 'Has thumbs capable of physically manipulating the thermostat.', description: 'Has thumbs capable of physically manipulating the thermostat.',
own100Achievement: 'hvacker100', own100Achievement: 'quade100',
}, },
hvacker: { hvacker: {
baseCost: 14_000_000_000_000, baseCost: 14_000_000_000_000,
earning: 65_000_000, earning: 65_000_000,
emoji: 'hvacker_angery', emoji: 'hvacker_angery',
description: 'Harness the power of the mad god himself.', description: 'Harness the power of the mad god himself.',
own100Achievement: 'creator100', own100Achievement: 'hvacker100',
}, },
creator: { creator: {
baseCost: 170_000_000_000_000, baseCost: 170_000_000_000_000,
earning: 430_000_000, earning: 430_000_000,
emoji: 'question', emoji: 'question',
description: 'The elusive creator of Hvacker takes a favorable look at your CPS.', description: 'The elusive creator of Hvacker takes a favorable look at your CPS.',
own100Achievement: 'smallBusiness100', own100Achievement: 'creator100',
}, },
smallBusiness: { smallBusiness: {
baseCost: 2_210_000_000_000_000, baseCost: 2_210_000_000_000_000,
earning: 2_845_000_000, earning: 2_845_000_000,
emoji: 'convenience_store', emoji: 'convenience_store',
description: 'The place where the creator of Hvacker goes to work.', description: 'The place where the creator of Hvacker goes to work.',
own100Achievement: 'bigBusiness100', own100Achievement: 'smallBusiness100',
}, },
bigBusiness: { bigBusiness: {
baseCost: 26_210_000_000_000_000, baseCost: 26_210_000_000_000_000,
earning: 23_650_000_000, earning: 23_650_000_000,
emoji: 'office', emoji: 'office',
description: 'The place where the smallBusiness goes to work.', description: 'The place where the smallBusiness goes to work.',
own100Achievement: 'ratGod', own100Achievement: 'bigBusiness100',
} },
government: {
baseCost: 367_210_000_000_000_000,
earning: 185_000_000_000,
emoji: 'japanese_castle',
description: 'By the people, for the people, etc. etc.',
own100Achievement: 'government100',
},
} }

View File

@ -0,0 +1,174 @@
const { game, getUser, saveGame, petBoost, getCoins, updateAll, getCPS, addCoins, commas} = require('./utils')
const slack = require('../../slack')
const maxValue = 10
const makePet = () => ({
food: 0,
fun: 0,
})
const bad = (text = '') => `
_^__^_
/ x x \\${text && ` "${text}"`}
>\\ o /<
----
`
const normal = (text = '') => `
_^__^_
/ o o \\${text && ` "${text}"`}
>\\ __ /<
----
`
const great = (text = '') => `
_^__^_
/ ^ ^ \\${text && ` "${text}"`}
>\\ \\__/ /<
----
`
const makeBar = (name, value) => {
const left = '#'.repeat(value)
const right = ' '.repeat(maxValue - value)
return `${name}:`.padEnd(6) + `[${left}${right}]`
}
const buildBlocks = ({text}) => [
{
type: 'section',
text: {
type: 'mrkdwn',
text
}
},
{
type: 'actions',
elements: [
buildBlock('Feed'),
buildBlock('Play'),
]
}]
const buildBlock = actionName => (
{
type: 'button',
text: {
type: 'plain_text',
text: actionName,
emoji: true
},
value: actionName,
action_id: actionName
}
)
// game.channelPets ??= {}
const petToText = (pet, additional, say) => {
const stats = Object.values(pet)
const hasTerribleStat = stats.filter(value => value < 1).length > 0
const averageStat = stats.reduce((total, current) => total + current, 0) / stats.length
let pic
if (hasTerribleStat && averageStat < 3) {
pic = bad
} else if (!hasTerribleStat && averageStat > 8) {
pic = great
} else {
pic = normal
}
let speech
if (pic === bad || Math.random() < 0.5) {
speech = pic()
} else {
speech = pic('Mrow')
}
additional ??= ''
if (additional) {
additional = `\n${additional}\n`
}
const text =
'```\n'
+ `Current HVAC Multiplier: ${petBoost()}x\n`
+ `${makeBar('Food', pet.food)}`
+ `\n${makeBar('Fun', pet.fun)}`
+ '\n'
+ speech
+ '```'
+ additional
saveGame('pet generation')
const ret = ({
text,
blocks: buildBlocks({text})
})
// If there's a say() this is a new message, otherwise, we're editing.
if (say) {
say(ret).then(({ channel, ts }) => {
// game.channelPets[channel] = ts
return updateAll({ name: 'pet', add: { channel, ts }})
}).catch(console.error)
}
return ret
}
const updateEveryone = async additional =>
updateAll({ name: 'pet', ...petToText(game.pet, additional) })
const statDown = () => {
const pet = (game.pet ??= makePet())
pet.food = Math.max(pet.food - 1, 0)
pet.fun = Math.max(pet.fun - 1, 0)
updateEveryone().catch(console.error)
setTimeout(() => {
Object.entries(game.users).forEach(([id, u]) => u.coins = getCoins(id))
statDown()
}, 1000 * 60 * 90) // Every 90 minutes
}
statDown()
const addInteraction = ({ actionId, perform }) =>
slack.app.action(actionId, async ({ body, ack, say, payload }) => {
try {
await ack()
game.pet ??= makePet()
const [everyone, local] = perform(game.pet, getUser(body.user.id))
await updateEveryone(everyone)
if (local) {
await say(local)
}
} catch (e) {
console.error(e)
}
})
const oneMinuteOfCps = user => Math.floor(getCPS(user) * 60)
addInteraction({ actionId: 'Feed', perform: (pet, user) => {
if (pet.food >= 10) {
return [`I'm too full to eat more, ${user.name}!`]
}
const oneMinute = oneMinuteOfCps(user)
addCoins(user, oneMinute)
pet.food += 1
return [`Thanks for the grub, ${user.name}!`, `Earned ${commas(oneMinute)} HVAC for feeding our pet!`]
} })
addInteraction({ actionId: 'Play', perform: (pet, user) => {
if (pet.fun >= 10) {
return [`I'm too tired for more games, ${user.name}.`]
}
const oneMinute = oneMinuteOfCps(user)
addCoins(user, oneMinute)
pet.fun += 1
return [`Thanks for playing, ${user.name}!`, `Earned ${commas(oneMinute)} HVAC for playing with our pet!`]
}})
module.exports = {
makePet,
petToText
}

View File

@ -24,8 +24,11 @@ const {
userHasCheckedQuackgrade, userHasCheckedQuackgrade,
fuzzyMatcher, fuzzyMatcher,
addCoins, addCoins,
game: { nfts, squad, users, horrors, stonkMarket }, game, updateAll
} = require('./utils') } = require('./utils')
const { nfts, squad, users, horrors, stonkMarket, pet } = game
const pets = require('./gotcha')
const slack = require('../../slack') const slack = require('../../slack')
const buyableItems = require('./buyableItems') const buyableItems = require('./buyableItems')
const upgrades = require('./upgrades') const upgrades = require('./upgrades')
@ -42,7 +45,7 @@ const settings = require('./settings')
// }) // })
// const read = () => { // const read = () => {
// readline.question(`What do YOU want? `, async want => { // readline.question(`What do YOU want? `, async want => {
// want && await slack.messageSage(want) // want && await slack.messageAdmin(want)
// read() // read()
// }) // })
// } // }
@ -56,8 +59,8 @@ const getUpgradeEmoji = upgrade => upgrade.emoji || buyableItems[upgrade.type].e
const upgradeText = (user, showOwned = false) => { const upgradeText = (user, showOwned = false) => {
const userDoesNotHave = ([upgradeName, upgrade]) => hasUpgrade(user, upgrade, upgradeName) === showOwned const userDoesNotHave = ([upgradeName, upgrade]) => hasUpgrade(user, upgrade, upgradeName) === showOwned
const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames()) const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
const format = ([key, value]) => `:${getUpgradeEmoji(value)}: *${key}* - ${commas(value.cost)}\n_${value.description}_` const format = ([, value]) => `:${getUpgradeEmoji(value)}: *${value.name}* - ${commas(value.cost)}\n_${value.description}_`
return '\n\n' + const subtotal = '\n\n' +
Object.entries(upgrades) Object.entries(upgrades)
.filter(userDoesNotHave) .filter(userDoesNotHave)
.filter(userMeetsCondition) .filter(userMeetsCondition)
@ -65,6 +68,7 @@ const upgradeText = (user, showOwned = false) => {
.join('\n\n') + .join('\n\n') +
'\n\n:grey_question::grey_question::grey_question:' + '\n\n:grey_question::grey_question::grey_question:' +
'\n\nJust type \'!upgrade upgrade_name\' to purchase' '\n\nJust type \'!upgrade upgrade_name\' to purchase'
return subtotal.trim()
} }
const hasUpgrade = (user, upgrade, upgradeName) => !!user.upgrades[upgrade.type]?.includes(upgradeName) const hasUpgrade = (user, upgrade, upgradeName) => !!user.upgrades[upgrade.type]?.includes(upgradeName)
@ -109,6 +113,15 @@ const commands = new Map()
let commandHelpText = '' let commandHelpText = ''
let shortCommandHelpText = 'Use `!help full` to show details for all commands, or `!<command> help` to show for just one.\n```' let shortCommandHelpText = 'Use `!help full` to show details for all commands, or `!<command> help` to show for just one.\n```'
const defaultAccess = { hidden: false, condition: alwaysAccessible } const defaultAccess = { hidden: false, condition: alwaysAccessible }
/**
*
* @param {[string]} commandNames
* @param {string} helpText
* @param {function({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted })} action: boolean
* @param {boolean} hidden
* @param condition
*/
const command = (commandNames, helpText, action, { hidden, condition } = defaultAccess) => { const command = (commandNames, helpText, action, { hidden, condition } = defaultAccess) => {
if (!hidden) { if (!hidden) {
console.log(`Initializing command '${commandNames[0]}'`) console.log(`Initializing command '${commandNames[0]}'`)
@ -157,7 +170,7 @@ const getHorrorMessageOdds = (offset = 0) => {
command( command(
['!odds'], ['!odds'],
'Show shuffle odds re: !horror', 'Show shuffle odds re: !horror',
async ({ say, args }) => { async ({ say, args, user }) => {
const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or about 1 in ${Math.round(1 / odds)}` const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or about 1 in ${Math.round(1 / odds)}`
if (!args[0]) { if (!args[0]) {
return say( return say(
@ -168,7 +181,7 @@ command(
//`Tomorrow's horror message odds will be ${percentOrOneIn(getHorrorMessageOdds(1))}` //`Tomorrow's horror message odds will be ${percentOrOneIn(getHorrorMessageOdds(1))}`
) )
} }
const num = parseAll(args[0], 99) const num = parseAll(args[0], 99, user)
return say( return say(
`Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n` `Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n`
//`Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}` //`Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}`
@ -178,9 +191,9 @@ command(
command( command(
['!shuffle'], ['!shuffle'],
'!shuffle daysFromNow message', '!shuffle daysFromNow message',
async ({ say, args }) => { async ({ say, args, user }) => {
const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or 1 in ${Math.round(1 / odds)}` const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or 1 in ${Math.round(1 / odds)}`
const num = parseAll(args[0], 99) const num = parseAll(args[0], 99, user)
const [, ...message] = args const [, ...message] = args
return say( return say(
`Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n` + `Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n` +
@ -213,12 +226,12 @@ if (settings.horrorEnabled) {
['!horror'], ['!horror'],
'help help help help help', 'help help help help help',
async ({ event, say }) => { async ({ event, say }) => {
if (event.user === slack.users.Sage) { if (event.user === slack.users.Admin) {
return slack.postToTechThermostatChannel(shufflePercent(event.text.substring(7).trim(), getShuffleOdds())) return slack.postToTechThermostatChannel(shufflePercent(event.text.substring(7).trim(), getShuffleOdds()))
} }
horrors.commandCalls ??= 0 horrors.commandCalls ??= 0
horrors.commandCalls += 1 horrors.commandCalls += 1
await slack.messageSage(`<@${event.user}> found !horror.`) await slack.messageAdmin(`<@${event.user}> found !horror.`)
await say('_Do you think you can help me?_') // TODO horror horrors change help to !help await say('_Do you think you can help me?_') // TODO horror horrors change help to !help
}, { hidden: true }) }, { hidden: true })
} }
@ -232,7 +245,7 @@ const buildHorrorSay = ({ say, event, commandName, c }) => async message => {
if (shuffled.length > 100 && Math.random() < getShuffleOdds()) { if (shuffled.length > 100 && Math.random() < getShuffleOdds()) {
const middle = (shuffled.length / 2) + Math.round((Math.random() - 1) * (shuffled.length / 5)) const middle = (shuffled.length / 2) + Math.round((Math.random() - 1) * (shuffled.length / 5))
shuffled = shuffled.substring(0, middle) + definitelyShuffle(getRandomFromArray(horrorMessages), shuffleOdds * 1.5) + shuffled.substring(middle) shuffled = shuffled.substring(0, middle) + definitelyShuffle(getRandomFromArray(horrorMessages), shuffleOdds * 1.5) + shuffled.substring(middle)
await slack.messageSage(`Just sent a hidden horror to ${slack.users[event.user]}:\n\n${shuffled}`) await slack.messageAdmin(`Just sent a hidden horror to ${slack.users[event.user]}:\n\n${shuffled}`)
} }
await say(shuffled) await say(shuffled)
} else { } else {
@ -244,6 +257,7 @@ const buildSayWithPayload = ({ say, event }) => async msg => {
const payload = { const payload = {
event: { event: {
text: event.text, text: event.text,
user: event.user
} }
} }
if (typeof(msg) === 'string') { if (typeof(msg) === 'string') {
@ -261,7 +275,7 @@ command(
['!!peter-griffin-family-guy'], ['!!peter-griffin-family-guy'],
'Delete', 'Delete',
async ({ say, user }) => { async ({ say, user }) => {
if (user.isDisabled === false) { if (user.isDisabled === false) { // As opposed to null/undefined
return say(`Silly, silly, ${user.name}.\nYou can't just leave us again.`) return say(`Silly, silly, ${user.name}.\nYou can't just leave us again.`)
} }
user.isDisabled = true user.isDisabled = true
@ -269,7 +283,14 @@ command(
return say('.') return say('.')
}, { hidden: true }) }, { hidden: true })
const garble = text => text.split('').map(() => '□').join('') const badChars = [' ', '□', '-']
const garble = text => {
let garbled
do {
garbled = text.split('').map(c => Math.random() < 0.075 ? getRandomFromArray(badChars) : c).join('')
} while (garbled.length > 0 && garbled === text)
return garbled
}
const noWinner = 'NO WINNER' const noWinner = 'NO WINNER'
@ -343,15 +364,39 @@ command(
} }
) )
const messageHandler = async ({ event, say, isRecycle = false }) => { const cursedPics = {
U0B8W0AF3: ['https://i.imgur.com/UNYKS0c.png'], //'Charles',
//U0B8RTK5L: '', //'Zane',
U02KYLVK1GV: ['https://i.imgur.com/VTSob8w.png', 'https://i.imgur.com/BdXqA5d.jpeg'], //'Quade',
UTDLFGZA5: ['https://i.imgur.com/YIrz4JQ.png'], // 'Tyler',
U017PG4EL1Y: ['https://i.imgur.com/d65EuaQ.png'],//'Max',
// hole viscera
U02AAB54V34: ['https://i.imgur.com/ewT3AoL.png', 'https://i.imgur.com/LLyzybV.png'] // 'Houston'
}
const messageHandler = async ({ event, say, isRecycle = false, skipCounting }) => {
if (event?.subtype === 'bot_message') { if (event?.subtype === 'bot_message') {
return botMessageHandler({ event, say }) return botMessageHandler({ event, say })
} }
const startTime = new Date()
const words = event?.text?.split(/\s+/) || [] const words = event?.text?.split(/\s+/) || []
const [commandName, ...args] = words const [commandName, ...args] = words
const c = commands.get(commandName) const c = commands.get(commandName)
if (!c && words[0]?.startsWith('!') && event.user !== slack.users.Sage) { let user = getUser(event.user)
return slack.messageSage(`${slack.users[event.user]} tried to use \`${event.text}\`, if you wanted to add that.`) if (user.isDisabled) {
return
}
user.name = slack.users[event.user]
if (!skipCounting && words[0]?.startsWith('!')) {
user.commandCounts ??= {}
user.commandCounts[words[0]] ??= 0
user.commandCounts[words[0]] += 1
}
if (!c && words[0]?.startsWith('!') && event.user !== slack.users.Admin) {
if (!slack.pollTriggers.includes(words[0])) {
return slack.messageAdmin(`${slack.users[event.user]} tried to use \`${event.text}\`, if you wanted to add that.`)
}
} }
const trueSay = say const trueSay = say
@ -361,27 +406,39 @@ const messageHandler = async ({ event, say, isRecycle = false }) => {
say = buildHorrorSay({say, event, args, commandName, c}) say = buildHorrorSay({say, event, args, commandName, c})
} }
let user = getUser(event.user) // if (user.isPrestiging) {
if (user.isDisabled) { // return say(`Finish prestiging first!`)
return // }
}
const hauntOdds = 0.03 const hauntOdds = 0.005
const disabledUsers = Object.entries(users).filter(([, user]) => user.isDisabled) const disabledUsers = Object.entries(users).filter(([, user]) => user.isDisabled)
let haunted = false let haunted = false
if (disabledUsers.length !== 0) { if (disabledUsers.length === 0) {
if (user.expectingPossession) { user.expectingPossession = false
} else {
const hauntless = ['!lore']
if (user.expectingPossession && !hauntless.includes(commandName)) {
console.log(`Haunting ${user.name}`)
user.expectingPossession = false user.expectingPossession = false
//saveGame() //saveGame()
haunted = true haunted = true
const [disabledId, disabledUser] = getRandomFromArray(disabledUsers) const [disabledId] = getRandomFromArray(disabledUsers)
event.user = disabledId event.user = disabledId
user = getUser(event.user) user = getUser(event.user)
if (Math.random() < 0.2) { const userInfo = await slack.app.client.users.info({
say = slack.buildSayPrepend({ say, prepend: `_You feel haunted..._\n_"Hey, it's me, ${(garble(disabledUser.name))}"_\n` }) user: disabledId
} else { })
say = slack.buildSayPrepend({ say, prepend: `_You feel haunted..._\n` }) say = async msg => {
let icon_url = userInfo.user.profile.image_original
if (cursedPics[event.user]?.length > 0) {
icon_url = getRandomFromArray(cursedPics[event.user])
}
trueSay({
text: msg,
username: garble(userInfo.user.profile.real_name),
icon_url
})
} }
} else if (Math.random() < hauntOdds) { } else if (Math.random() < hauntOdds) {
user.expectingPossession = true user.expectingPossession = true
@ -390,17 +447,17 @@ const messageHandler = async ({ event, say, isRecycle = false }) => {
say = slack.buildSayPrepend({ say, prepend: `_You feel a chill..._\n` }) say = slack.buildSayPrepend({ say, prepend: `_You feel a chill..._\n` })
} }
} }
} else {
user.expectingPossession = false
} }
user.coins = getCoins(event.user) Object.entries(users).forEach(([id, usr]) => usr.coins = getCoins(id))
const canUse = await c?.condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Sage) }) //user.coins = getCoins(event.user)
const isAdmin = event.user?.includes(slack.users.Admin)
const canUse = await c?.condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin })
if (!canUse) { if (!canUse) {
// const matcher = fuzzyMatcher(commandName) // const matcher = fuzzyMatcher(commandName)
// for (const key of commands.keys()) { // for (const key of commands.keys()) {
// if (matcher.test(key)) { // if (matcher.test(key)) {
// const fetched = commands.get(key) // const fetched = commands.get(key)
// if (!fetched.hidden && await fetched.condition({ event, say: () => {}, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Sage) })) { // if (!fetched.hidden && await fetched.condition({ event, say: () => {}, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Admin) })) {
// //return say(`Did you mean '${key}'?`) // //return say(`Did you mean '${key}'?`)
// } // }
// } // }
@ -430,7 +487,7 @@ const messageHandler = async ({ event, say, isRecycle = false }) => {
return return
} }
await c.action({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted }) await c.action({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted, isAdmin })
if (!isRecycle) { if (!isRecycle) {
const userQuackgrades = (user.quackUpgrades?.lightning || []).map(name => quackStore[name]) const userQuackgrades = (user.quackUpgrades?.lightning || []).map(name => quackStore[name])
const defaultOdds = 0.005 const defaultOdds = 0.005
@ -443,7 +500,8 @@ const messageHandler = async ({ event, say, isRecycle = false }) => {
setTimeout(() => lightning({ channel: event.channel, say, trueSay, words, user }), 10000) setTimeout(() => lightning({ channel: event.channel, say, trueSay, words, user }), 10000)
} }
} }
saveGame() const endTime = new Date()
saveGame(`command ${event.text} finished in ${endTime - startTime}ms`)
} }
slack.onReaction(async ({ event }) => { slack.onReaction(async ({ event }) => {
@ -517,17 +575,10 @@ const lightning = async ({ user, ms = 5000, channel, multiplier = 1 }) => {
channel: message.channel, channel: message.channel,
ts: message.ts, ts: message.ts,
}) })
// await slack.messageSage(`${user.name} failed to bottle some lighting!`) // await slack.messageAdmin(`${user.name} failed to bottle some lighting!`)
}, msToBottle) }, msToBottle)
} }
const dedicatedPlayers = [
slack.users.Sage,
slack.users.Houston,
slack.users.Adam,
slack.users.Fernando,
]
command( command(
['!bolt'], ['!bolt'],
'Send a lighting strike to the given player.', 'Send a lighting strike to the given player.',
@ -564,11 +615,15 @@ slack.app.action('lightningStrike', async ({ body, ack }) => {
const c = getCoins(body.user.id) const c = getCoins(body.user.id)
const user = getUser(body.user.id) const user = getUser(body.user.id)
const secondsOfCps = seconds => Math.floor(getCPS(user) * seconds) const secondsOfCps = seconds => Math.floor(getCPS(user) * seconds)
let payout = Math.floor(c * 0.10) + secondsOfCps(60 * 30) let payout = secondsOfCps(60 * 30)
if (payout > (0.2 * c)) {
payout = 0.2 * c
}
payout = (500 + chaosFilter(payout, 1, user)) * strikes[body.message.ts] payout = (500 + chaosFilter(payout, 1, user)) * strikes[body.message.ts]
addCoins(user, (c + payout) - user.coins) addCoins(user, (c + payout) - user.coins)
delete strikes[body.message.ts] delete strikes[body.message.ts]
saveGame() saveGame('user bottled a lightning strike')
await slack.app.client.chat.update({ await slack.app.client.chat.update({
channel: body.channel.id, channel: body.channel.id,
@ -577,7 +632,7 @@ slack.app.action('lightningStrike', async ({ body, ack }) => {
blocks: [] blocks: []
}) })
await ack() await ack()
return slack.messageSage(`Lighting bottled by <@${body.user.id}>`) return slack.messageAdmin(`Lighting bottled by <@${body.user.id}>`)
}) })
slack.onMessage(async msg => { slack.onMessage(async msg => {
@ -785,7 +840,7 @@ command(
command( command(
['!a', '!ach', '!achievements'], ['!a', '!ach', '!achievements'],
'List your glorious achievements', 'List your glorious achievements',
async ({ event, say, user }) => { async ({ event, say, user, args, isAdmin }) => {
const achievementCount = Object.keys(user.achievements).length const achievementCount = Object.keys(user.achievements).length
const prefix = `You have ${achievementCount} achievements!\n\n` const prefix = `You have ${achievementCount} achievements!\n\n`
const mult = (Math.pow(1.01, achievementCount) - 1) * 100 const mult = (Math.pow(1.01, achievementCount) - 1) * 100
@ -880,36 +935,49 @@ command(['!cps'],
const doMine = async ({ user, userId, say }) => { const doMine = async ({ user, userId, say }) => {
const random = Math.random() const random = Math.random()
const c = user.coins const c = user.coins
const secondsOfCps = seconds => Math.floor(getCPS(user) * seconds) const secondsOfCps = (seconds, ceiling) => {
const s = Math.floor(getCPS(user) * seconds)
const ceil = ceiling * c
if (s > ceil) {
return ceil
}
return s
}
let diff let diff
let prefix let prefix
if (random > 0.9947) { if (random > 0.9947) {
diff = 500 + Math.floor(c * 0.10) + secondsOfCps(60 * 30) diff = 500 + secondsOfCps(60 * 60, 0.2)
prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n` prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n`
addAchievement(user, 'luckyGem', say) addAchievement(user, 'luckyGem', say)
await slack.messageSage(`${slack.users[userId]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`) await slack.messageAdmin(`${slack.users[userId]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`)
} else if (random > 0.986) { } else if (random > 0.986) {
diff = 50 + Math.floor(c * 0.025) + secondsOfCps(60) diff = 50 + secondsOfCps(60 * 5, 0.1)
prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n` prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n`
addAchievement(user, 'goldBrick', say) addAchievement(user, 'goldBrick', say)
} else if (random > 0.94) { } else if (random > 0.94) {
diff = 10 + Math.floor(c * 0.01) + secondsOfCps(10) diff = 10 + secondsOfCps(60, 0.1)
prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n` prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n`
addAchievement(user, 'greenCoin', say) addAchievement(user, 'greenCoin', say)
} else { } else {
prefix = 'You mined one HVAC.\n' const miningUpgrades = (user.upgrades.mining || []).map(name => upgrades[name])
diff = 1 diff = miningUpgrades.reduce((total, upgrade) => upgrade.effect(total, user), 1)
console.log({ miningUpgrades, diff, user: user.upgrades.mining })
prefix = `You mined ${commas(diff)} HVAC.\n`
} }
addCoins(user, diff) addCoins(user, diff)
//saveGame() //saveGame()
return `${prefix}You now have ${commas(user.coins)} HVAC coin${c !== 1 ? 's' : ''}. Spend wisely.` return `${prefix}You now have ${commas(user.coins)} HVAC coin${c !== 1 ? 's' : ''}. Spend wisely.`
} }
let lbIndex = 0
command( command(
['!c', '!coin', '!mine', '!'], ['!c', '!coin', '!mine', '!'],
'Mine HVAC coins', 'Mine HVAC coins',
async ({ say, user, userId }) => { async ({ say, user, userId }) => {
await say(await doMine({ user, userId, say })) await say(await doMine({ user, userId, say }))
if ((lbIndex++) % 5 == 0) {
return updateAllLeaderboards()
}
} }
) )
@ -919,10 +987,11 @@ command(
async ({ event, args, trueSay }) => { async ({ event, args, trueSay }) => {
const [impersonating, ...newWords] = args const [impersonating, ...newWords] = args
event.user = idFromWord(impersonating) event.user = idFromWord(impersonating)
getUser(event.user)
const isDisabled = users[event.user].isDisabled const isDisabled = users[event.user].isDisabled
users[event.user].isDisabled = false users[event.user].isDisabled = false
event.text = newWords.join(' ') event.text = newWords.join(' ')
await messageHandler({ event, say: trueSay, isRecycle: false}) await messageHandler({ event, say: trueSay, isRecycle: false, skipCounting: true })
users[event.user].isDisabled = isDisabled users[event.user].isDisabled = isDisabled
}, adminOnly) }, adminOnly)
@ -951,21 +1020,40 @@ command(
if (user.isDisabled) { if (user.isDisabled) {
user.isDisabled = false user.isDisabled = false
//saveGame() //saveGame()
addAchievement(user, 'theOtherSide', slack.messageSage) addAchievement(user, 'theOtherSide', slack.messageAdmin)
await slack.postToTechThermostatChannel(`_${user.name} has returned..._`) await slack.postToTechThermostatChannel(`_${user.name} has returned..._`)
} }
} }, adminOnly)
)
command(
['!disable'],
'Disable the given user',
async ({ args }) => {
const user = getUser(idFromWord(args[0]))
user.isDisabled = true
}, adminOnly)
command( command(
['!g', '!gamble'], ['!g', '!gamble'],
'Gamble away your HVAC\n' + 'Gamble away your HVAC\n' +
' To use, say \'gamble coin_amount\' or \'!gamble all\'', ' To use, say \'gamble coin_amount\' or \'!gamble all\'',
async ({ say, args, user }) => { async ({ say, trueSay, args, user, event }) => {
const requestedWager = parseAll(args.join(' '), user.coins) if (event.text?.includes(`y'all`)) {
if (event.user !== slack.users.Admin) {
return say('Perhaps another time...')
}
await say(`Gambling the souls of all players...`)
setTimeout(async () => {
say('You bet the souls of your coworkers and won 1 HVAC!')
addCoins(user, 1)
}, 25000)
return
}
const argText = args.join(' ')
const requestedWager = parseAll(argText, user.coins, user)
const n = (chaosFilter(requestedWager, 0.2, user, user.coins) + requestedWager) / 2 const n = (chaosFilter(requestedWager, 0.2, user, user.coins) + requestedWager) / 2
if (!n || n < 0) { if (!n || n < 0) {
return say(`Invalid number '${n}'`) return say(`Invalid number '${argText}'`)
} }
if (user.coins < n) { if (user.coins < n) {
return say(`You don't have that many coins! You have ${commas(user.coins)}.`) return say(`You don't have that many coins! You have ${commas(user.coins)}.`)
@ -979,6 +1067,9 @@ command(
if (n >= 100_000_000_000_000_000) { if (n >= 100_000_000_000_000_000) {
addAchievement(user, 'mondoBets', say) addAchievement(user, 'mondoBets', say)
} }
if (n >= 100_000_000_000_000_000_000) {
addAchievement(user, 'sigmaBets', say)
}
user.coins -= n user.coins -= n
let outcome let outcome
if (Math.random() > 0.5) { if (Math.random() > 0.5) {
@ -991,30 +1082,45 @@ command(
//saveGame() //saveGame()
await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`) await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`)
if (outcome === 'lost' && user.lostBetMessage) { if (outcome === 'lost' && user.lostBetMessage) {
await say(user.lostBetMessage) await trueSay(user.lostBetMessage)
} else if (outcome === 'won' && user.wonBetMessage) { } else if (outcome === 'won' && user.wonBetMessage) {
await say(user.wonBetMessage) await trueSay(user.wonBetMessage)
} }
return updateAllLeaderboards()
} }
) )
const emojiRegex = /^:[^:\s]*:$/
const validEmoji = async emojiText => {
emojiText = emojiText?.trim()
if (!emojiText || !emojiRegex.test(emojiText)) {
return false
}
const validEmojis = (await getEmojis()).emoji
const noColons = emojiText.replace(/:/g, '')
// console.log('validEmojis', validEmojis)
return !!validEmojis[noColons]
}
const getEmojis = async () => await slack.app.client.emoji.list()
command( command(
['!setloss'], ['!setloss'],
'!setloss <emoji>', '!setloss <emoji>',
async ({ args, user, say }) => { async ({ args, user, say }) => {
const emoji = args[0] const emoji = args[0]
if (!emoji || !emoji.startsWith(':') || !emoji.endsWith(':') || emoji.includes(' ') || emoji.includes('\n')) { if (!await validEmoji(emoji)) {
return say(`Argument must be a single emoji!`) return say(`Argument must be a single emoji!`)
} }
user.lostBetMessage = emoji user.lostBetMessage = emoji
}, {hidden: true}) }, {hidden: true})
command( command(
['!setwon'], ['!setwon', '!setwin'],
'!setwon <emoji>', '!setwon <emoji>',
async ({ args, user, say }) => { async ({ args, user, say }) => {
const emoji = args[0] const emoji = args[0]
if (!emoji || !emoji.startsWith(':') || !emoji.endsWith(':')) { if (!await validEmoji(emoji)) {
return say(`Argument must be a single emoji!`) return say(`Argument must be a single emoji!`)
} }
user.wonBetMessage = emoji user.wonBetMessage = emoji
@ -1052,55 +1158,68 @@ command(
await say(upgradeText(user, true)) await say(upgradeText(user, true))
}, dmsOnly) }, dmsOnly)
const upgradeText2 = user => { const upgradeText2 = (user, extraMessage = '') => {
const userDoesNotHave = ([upgradeName, upgrade]) => !hasUpgrade(user, upgrade, upgradeName) const userDoesNotHave = ([upgradeName, upgrade]) => !hasUpgrade(user, upgrade, upgradeName)
const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames()) const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
return ({ return ({
text: upgradeText(user, false), text: (extraMessage && extraMessage + '\n') + upgradeText(user, false),
blocks: Object.entries(upgrades) blocks: [
(extraMessage && {
type: 'section',
text: {
type: 'mrkdwn',
text: extraMessage + '\n'
},
}),
...Object.entries(upgrades)
.filter(userDoesNotHave) .filter(userDoesNotHave)
.filter(userMeetsCondition) .filter(userMeetsCondition)
.map(([upgradeName]) => upgradeBlock(upgradeName)) .map(([upgradeName]) => upgradeBlock(upgradeName))
].filter(block => block)
}) })
} }
command( command(
['!upgrade', '!u'], ['!upgrade', '!u'],
'Improve the performance of your HVAC-generators.\n' + 'Improve the performance of your HVAC-generators.\n' +
' Say \'!upgrade\' to list available upgrades, or \'!upgrade upgrade_name\' to purchase.', ' Say \'!upgrade\' to list available upgrades, or \'!upgrade upgrade_name\' to purchase directly.',
async ({ say, args, user }) => { async ({ say, args, user }) => {
const upgradeName = args[0] if (!args[0]) {
if (!upgradeName) {
return say(upgradeText2(user)) return say(upgradeText2(user))
} }
const upgrade = upgrades[upgradeName] console.log({args: args.join(' ')})
if (!upgrade) { const matcher = fuzzyMatcher(args.join(' '))
return say('An upgrade with that name does not exist!') const u = Object.entries(upgrades).find(([name, upgrade]) => matcher.test(name) || matcher.test(upgrade.name))
if (!u) {
return say(`Could not find an upgrade matching "${args.join(' ')}"!`)
} }
const [id, upgrade] = u
if (!user.upgrades[upgrade.type]) { if (!user.upgrades[upgrade.type]) {
user.upgrades[upgrade.type] = [] user.upgrades[upgrade.type] = []
} }
if (hasUpgrade(user, upgrade, upgradeName)) { if (hasUpgrade(user, upgrade, id)) {
return say('You already have that upgrade!') return say(`You already have ${upgrade.name}!`)
} }
if (!upgrade.condition(user, getCompletedSquadgradeNames())) { if (!upgrade.condition(user, getCompletedSquadgradeNames())) {
return say('That item does not exist!') return say('That item does not exist!')
} }
const c = user.coins const c = user.coins
if (c < upgrade.cost) { if (c < upgrade.cost) {
return say(`You don't have enough coins! You have ${commas(c)}, but you need ${commas(upgrade.cost)}`) return say(`You don't have enough coins to buy ${upgrade.name}!\nYou have ${commas(c)}, but you need ${commas(upgrade.cost)}`)
} }
user.coins -= upgrade.cost user.coins -= upgrade.cost
user.upgrades[upgrade.type].push(upgradeName) user.upgrades[upgrade.type].push(id)
//saveGame() //saveGame()
await say(`You bought ${upgradeName}!`) await say(`You bought ${id}!`)
}, dmsOnly) }, dmsOnly)
const upgradeBlock = upgradeName => ({ const upgradeBlock = upgradeName => {
const upgrade = upgrades[upgradeName]
return ({
type: 'section', type: 'section',
text: { text: {
type: 'mrkdwn', type: 'mrkdwn',
text: `${upgradeName} :${(buyableItems[upgrades[upgradeName].type].emoji)}: - H${commas(upgrades[upgradeName].cost)}\n_${upgrades[upgradeName].description}_` text: `${upgrade.name} :${buyableItems[upgrade.type]?.emoji || upgrade.emoji}: - H${commas(upgrade.cost)}\n_${upgrade.description}_`
}, },
accessory: { accessory: {
type: 'button', type: 'button',
@ -1112,7 +1231,8 @@ const upgradeBlock = upgradeName => ({
value: 'upgrade_' + upgradeName, value: 'upgrade_' + upgradeName,
action_id: 'upgrade_' + upgradeName action_id: 'upgrade_' + upgradeName
} }
}) })
}
const upgradeButton = async ({ body, ack, say, payload }) => { const upgradeButton = async ({ body, ack, say, payload }) => {
await ack() await ack()
@ -1121,16 +1241,19 @@ const upgradeButton = async ({ body, ack, say, payload }) => {
const event = { const event = {
user: body.user.id user: body.user.id
} }
const user = getUser(event.user) const user = getUser(event.user, true)
const words = ['!upgrade', upgrade] const words = ['!upgrade', upgrade]
const [commandName, ...args] = words const [commandName, ...args] = words
let extraMessage = ''
say = async text => extraMessage = text
await commands.get('!u').action({ event, say, words, args, commandName, user }) await commands.get('!u').action({ event, say, words, args, commandName, user })
//const highestCoins = user.highestEver || user.coins || 1 //const highestCoins = user.highestEver || user.coins || 1
await slack.app.client.chat.update({ await slack.app.client.chat.update({
channel: body.channel.id, channel: body.channel.id,
ts: body.message.ts, ts: body.message.ts,
...upgradeText2(user) ...upgradeText2(user, extraMessage)
}) })
await updateAllLeaderboards()
} }
Object.keys(upgrades).forEach(upgradeName => slack.app.action('upgrade_' + upgradeName, upgradeButton)) Object.keys(upgrades).forEach(upgradeName => slack.app.action('upgrade_' + upgradeName, upgradeButton))
@ -1146,15 +1269,13 @@ const getCurrentSquadgrade = () => {
} }
return { return {
name, name,
upgrade,
remaining: squad.upgrades[name], remaining: squad.upgrades[name],
emoji: upgrade.emoji, emoji: upgrade.emoji,
description: upgrade.description description: upgrade.description
} }
} }
const squadgradeText = ([name, { emoji, description }]) =>
`:${emoji}: *${name}*\n_${description}_`
const squadText = () => { const squadText = () => {
const current = getCurrentSquadgrade() const current = getCurrentSquadgrade()
if (current) { if (current) {
@ -1163,8 +1284,6 @@ const squadText = () => {
return currentUpgradeText(current) return currentUpgradeText(current)
} }
return 'No more squadgrades currently available.' return 'No more squadgrades currently available.'
// const squadIsMissing = ([name, upgrade]) => squad.upgrades[name] === false
// return squadgradeText(Object.entries(squadUpgrades).find(squadIsMissing))
} }
command( command(
@ -1181,7 +1300,7 @@ command(
return say('No squadgrades are currently available') return say('No squadgrades are currently available')
} }
const currentCoins = user.coins const currentCoins = user.coins
let amount = parseAll(args.join(' '), currentCoins) let amount = parseAll(args.join(' '), currentCoins, user)
if (amount > currentCoins) { if (amount > currentCoins) {
return say(`You don't have that much HVAC! You have ${currentCoins}.`) return say(`You don't have that much HVAC! You have ${currentCoins}.`)
} }
@ -1210,7 +1329,7 @@ command(
} }
) )
const buyRoute = require('./buy') const { buyRoute, leaderboardUpdater } = require('./buy')
command( command(
['!buy', '!b', '?b', '?buy'], ['!buy', '!b', '?b', '?buy'],
'Buy new items to earn HVAC with\n' + 'Buy new items to earn HVAC with\n' +
@ -1270,7 +1389,7 @@ command(
} }
let [target, ...amountText] = args let [target, ...amountText] = args
amountText = amountText.join(' ') amountText = amountText.join(' ')
const amount = parseAll(amountText, user.coins) const amount = parseAll(amountText, user.coins, user)
const targetId = idFromWord(target) const targetId = idFromWord(target)
if (!amount || amount < 0) { if (!amount || amount < 0) {
return say('Amount must be a positive integer!') return say('Amount must be a positive integer!')
@ -1281,10 +1400,6 @@ command(
if (user.coins < amount) { if (user.coins < amount) {
return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`) return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`)
} }
const date = new Date()
if (targetId === slack.users.Nik && date.getFullYear() === 2022 && date.getMonth() === 4) {
return say(`Your generosity is appreciated, but let the guy play for a bit!`)
}
if (amountText === 'all' && slack.users.Tyler === targetId) { if (amountText === 'all' && slack.users.Tyler === targetId) {
addAchievement(user, 'walmartGiftCard', say) addAchievement(user, 'walmartGiftCard', say)
} }
@ -1320,7 +1435,7 @@ command(
['!gimme'], ['!gimme'],
'Give self x coins', 'Give self x coins',
async ({ say, args, user }) => { async ({ say, args, user }) => {
const increase = parseInt(args[0].replace(/,/g, '')) const increase = parseAll(args.join(' '))
addCoins(user, increase) addCoins(user, increase)
await say(`You now have ${user.coins} HVAC.`) await say(`You now have ${user.coins} HVAC.`)
}, testOnly) }, testOnly)
@ -1350,23 +1465,23 @@ const buildPEmoji = name => {
return ret return ret
} }
const prestigeEmojis = [ const prestigeEmojis = [
buildPEmoji('rock'), 'rock',
buildPEmoji('wood'), 'wood',
buildPEmoji('seedling'), 'seedling',
buildPEmoji('evergreen_tree'), 'evergreen_tree',
buildPEmoji('hibiscus'), 'hibiscus',
buildPEmoji('thunder_cloud_and_rain'), 'thunder_cloud_and_rain',
buildPEmoji('rainbow'), 'rainbow',
buildPEmoji('star'), 'star',
buildPEmoji('dizzy'), 'dizzy',
buildPEmoji('sparkles'), 'sparkles',
buildPEmoji('star2'), 'star2',
buildPEmoji('stars'), 'stars',
buildPEmoji('comet'), 'comet',
buildPEmoji('night_with_stars'), 'night_with_stars',
buildPEmoji('milky_way'), 'milky_way',
buildPEmoji('eye'), 'eye',
] ].map(buildPEmoji)
const prestigeEmoji = user => { const prestigeEmoji = user => {
const p = user.prestige || 0 const p = user.prestige || 0
@ -1388,7 +1503,7 @@ command(
`Show the emoji for each prestige level you've been through.`, `Show the emoji for each prestige level you've been through.`,
async ({ say, user, event, args }) => { async ({ say, user, event, args }) => {
let p = user.prestige || 0 let p = user.prestige || 0
if (event.user === slack.users.Sage && args[0] === 'all') { if (event.user === slack.users.Admin && args[0] === 'all') {
p = 99999999 p = 99999999
} }
let message = '' let message = ''
@ -1402,16 +1517,18 @@ command(
}, prestigeOnly) }, prestigeOnly)
command( command(
['!leaderboard', '!lb'], ['!pet'],
'Show the top HVAC-earners, ranked by prestige, then CPS', `Take care of the office pet!\nPet bonuses are shared between all players!`,
async ({ say, user }) => { async ({ say, user, event, args }) => {
// if ((event.user === slack.users.Houston || event.user === slack.users.Sage) && event.channel_type.includes('im')) { pets.petToText(pet, null, say)
// return say('```' + `Hvacker - 9 souls - Taking from them whatever it desires\nSome other losers - who cares - whatever` + '```') })
// }
const strike = user => user.isDisabled ? '~' : ''
const generateLeaderboard = ({ args }) => {
let index = 1 let index = 1
await say( return Object.entries(users)
Object.entries(users) .filter(([, user]) => (!user.isDisabled || args[0] === 'all') && (Object.entries(user.items).length > 0 || user.prestige))
.filter(([, user]) => !user.isDisabled && (Object.entries(user.items).length > 0 || user.prestige))
.sort(([id, user1], [id2, user2]) => { .sort(([id, user1], [id2, user2]) => {
const leftPrestige = getUser(id).prestige const leftPrestige = getUser(id).prestige
const rightPrestige = getUser(id2).prestige const rightPrestige = getUser(id2).prestige
@ -1423,21 +1540,43 @@ command(
} }
return getCPS(user1) > getCPS(user2) return getCPS(user1) > getCPS(user2)
}) })
.map(([id, u]) => `${index++}. ${slack.users[id] || '???'} ${prestigeEmoji(u) || '-'} ${commas(getCPS(getUser(id)))} CPS - ${commas(getCoins(id))} HVAC`) .map(([id, u]) => `${strike(u)}${index++}. ${slack.users[id] || '???'} ${prestigeEmoji(u) || '-'} ${commas(getCPS(getUser(id)))} CPS - ${commas(getCoins(id))} HVAC${strike(u)}`)
.join('\n') .join('\n')
).then(() => addAchievement(user, 'leaderBoardViewer', say)) }
command(
['!leaderboard', '!lb'],
'Show the top HVAC-earners, ranked by prestige, then CPS',
async ({ say, user, args }) => {
// if ((event.user === slack.users.Houston || event.user === slack.users.Admin) && event.channel_type.includes('im')) {
// return say('```' + `Hvacker - 9 souls - Taking from them whatever it desires\nSome other losers - who cares - whatever` + '```')
// }
game.leaderboardChannels ??= {}
await say(generateLeaderboard({ args })).then(({ channel, ts }) => {
addAchievement(user, 'leaderBoardViewer', say)
game.leaderboardChannels[channel] = ts
return updateAllLeaderboards({ channel, ts })
})
} }
) )
const updateAllLeaderboards = async (add) => {
const currentBoard = generateLeaderboard({ args: [] })
return updateAll({ name: 'leaderboard', text: currentBoard, add })
}
leaderboardUpdater.updateAllLeaderboards = updateAllLeaderboards
const hints = [] const hints = []
const oneShot = (name, helpText, message, achievementName) => { const oneShot = (name, helpText, message, achievementName) => {
if (helpText) { if (helpText) {
hints.push(helpText) hints.push(helpText)
} }
const names = Array.isArray(name) ? name : [name]
command([name], helpText, async ({ say, user }) => { command(names, helpText, async ({ say, user }) => {
await say(message) await say(message)
await slack.messageSage(`Wow buddy they like your ${name} joke.`) await slack.messageAdmin(`Wow buddy they like your ${name} joke.`)
if (achievementName) { if (achievementName) {
addAchievement(user, achievementName, say) addAchievement(user, achievementName, say)
} }
@ -1449,9 +1588,10 @@ oneShot('!santa', 'Ho ho ho!', '<https://i.imgur.com/dBWgFfQ.png|I\'m Santa Quac
oneShot('!sugma', 'Not very original.', ':hvacker_angery:') oneShot('!sugma', 'Not very original.', ':hvacker_angery:')
oneShot('!pog', 'One poggers hvacker', '<https://i.imgur.com/XCg7WDz.png|poggers>') oneShot('!pog', 'One poggers hvacker', '<https://i.imgur.com/XCg7WDz.png|poggers>')
oneShot('!ligma', 'Not very original.', '<https://i.imgur.com/i1YtW7m.png|no>') oneShot('!ligma', 'Not very original.', '<https://i.imgur.com/i1YtW7m.png|no>')
oneShot('!dab', 'ACTIVATE COOL GUY MODE', '<https://i.imgur.com/FKYdeqo.jpg|I go XD style>', 'certifiedCoolGuy') oneShot(['!dab', '!dabs'], 'ACTIVATE COOL GUY MODE', '<https://i.imgur.com/FKYdeqo.jpg|I go XD style>', 'certifiedCoolGuy')
oneShot('!based', 'Sorry, it\'s a little hard to hear you!', '<https://i.imgur.com/IUX6R26.png|What?>') oneShot('!based', 'Sorry, it\'s a little hard to hear you!', '<https://i.imgur.com/IUX6R26.png|What?>')
oneShot('!shrek', 'Is love and is life.', '<https://i.imgur.com/QwuCQZA.png|Donkey!>') oneShot('!shrek', 'Is love and is life.', '<https://i.imgur.com/QwuCQZA.png|Donkey!>')
oneShot('!sugondese', 'I don\'t like you.', '<https://i.imgur.com/VCvfvdz.png|rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr>')
// oneShot('!imaginedragons', 'The worst part about any group of white people is that they could be Imagine Dragons and you\'d have no way of knowing.', '<https://i.imgur.com/QwuCQZA.png|Donkey!>') // oneShot('!imaginedragons', 'The worst part about any group of white people is that they could be Imagine Dragons and you\'d have no way of knowing.', '<https://i.imgur.com/QwuCQZA.png|Donkey!>')
command( command(
@ -1493,13 +1633,27 @@ command(
'Confirm your prestige activation.', 'Confirm your prestige activation.',
prestige.prestigeConfirmRoute, { hidden: true }) prestige.prestigeConfirmRoute, { hidden: true })
command(
['!pbeta'],
'Confirm your prestige activation.',
async ({ event, say, user, YEET }) => {
await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
}, { hidden: true })
command( command(
['!quack', '!quackstore'], ['!quack', '!quackstore'],
'Spend your prestigious quackings\n\n' + 'Confirm your prestige activation.',
'Say \'!quack upgrade_name\' to purchase a quackgrade', async ({ event, say, user, YEET }) => {
prestige.quackStoreRoute, await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
prestigeOnly }, prestigeOnly)
)
// command(
// ['!quack', '!quackstore'],
// 'Spend your prestigious quackings\n\n' +
// 'Say \'!quack upgrade_name\' to purchase a quackgrade',
// prestige.quackStoreRoute,
// prestigeOnly
// )
command( command(
['!myquack', '!myq'], ['!myquack', '!myq'],
@ -1592,7 +1746,7 @@ command(
return say('What, are you trying to steal from yourself? What, are you stupid?') return say('What, are you trying to steal from yourself? What, are you stupid?')
} }
if (!targetId) { if (!targetId) {
targetId = slack.users.Sage targetId = slack.users.Admin
} }
if (user.coins < amount) { if (user.coins < amount) {
return return
@ -1609,7 +1763,7 @@ command(
async ({ event, say, user }) => { async ({ event, say, user }) => {
const currentDate = new Date().getDate() const currentDate = new Date().getDate()
const lastLotto = user.lastLotto === undefined ? currentDate - 1 : user.lastLotto const lastLotto = user.lastLotto === undefined ? currentDate - 1 : user.lastLotto
if (lastLotto === currentDate && event.user !== slack.users.Sage) { if (lastLotto === currentDate && event.user !== slack.users.Admin) {
return say('Hey, only one lotto ticket per day, alright?') return say('Hey, only one lotto ticket per day, alright?')
} }
let msg let msg
@ -1635,8 +1789,8 @@ command(
command( command(
['!giveach'], ['!giveach'],
'!giveach @player ach_name', '!giveach @player ach_name',
async ({ args, say, user }) => { async ({ args, say, }) => {
addAchievement(user, args[1], say) addAchievement(getUser(idFromWord(args[0])), args[1], say)
//saveGame() //saveGame()
}, adminOnly) }, adminOnly)
@ -1646,6 +1800,45 @@ command(
async ({ args, say }) => say(`<@${args[0]}>`), async ({ args, say }) => say(`<@${args[0]}>`),
adminOnly) adminOnly)
command(
['!!postas'],
'',
async ({ args }) => {
let channel = slack.temperatureChannelId
if (args[0].startsWith('<#')) {
channel = args[0].substring(2, args[0].indexOf('|'))
const [, ...rest] = args
args = rest
}
if (args[0].startsWith('<@')) {
channel = args[0].substring(2, args[0].length - 1)
const [, ...rest] = args
args = rest
}
console.log({args, channel})
const target = idFromWord(args[0])
const [, ...rest] = args
const userInfo = await slack.app.client.users.info({
user: target
})
console.log(userInfo)
return slack.app.client.chat.postMessage({
channel,
text: rest.join(' '),
username: userInfo.user.profile.real_name,
icon_url: userInfo.user.profile.image_original
})
},
adminOnly)
command(
['!!startpoll'],
'',
async ({ args, say }) => {
},
adminOnly)
command( command(
['!ngift'], ['!ngift'],
'!ngift player_id nft_name', '!ngift player_id nft_name',
@ -1741,20 +1934,21 @@ const updateStonkPrices = () => {
for (let i = stonkMarket.lastDay; i < today; i++) { for (let i = stonkMarket.lastDay; i < today; i++) {
console.log('set lastPrice') console.log('set lastPrice')
stonk.lastPrice = stonk.price stonk.lastPrice = stonk.price
console.log(stonk.pattern, stonkPatterns)
stonk.price *= 1 + (stonkPatterns[stonk.pattern][stonk.index] / 100) stonk.price *= 1 + (stonkPatterns[stonk.pattern][stonk.index] / 100)
stonk.index++ stonk.index++
if (stonk.index >= stonkPatterns[stonk.pattern].length) { if (stonk.index >= stonkPatterns[stonk.pattern]?.length) {
stonk.index = 0 stonk.index = 0
stonk.pattern = nextPattern(stonk.pattern) stonk.pattern = nextPattern(stonk.pattern)
} }
} }
}) })
stonkMarket.lastDay = today stonkMarket.lastDay = today
//saveGame(true) //saveGame(null, true)
} }
const buyStonks = (user, stonkName, quantityPhrase) => { const buyStonks = (user, stonkName, quantityPhrase) => {
const quantity = parseAll(quantityPhrase, Math.floor(user.coins / stonkMarket.stonks[stonkName].price)) const quantity = parseAll(quantityPhrase, Math.floor(user.coins / stonkMarket.stonks[stonkName].price), user)
if (!quantity) { if (!quantity) {
return 'Quantity must be positive integer!' return 'Quantity must be positive integer!'
} }
@ -1773,7 +1967,7 @@ const buyStonks = (user, stonkName, quantityPhrase) => {
const sellStonks = (user, stonkName, quantityPhrase) => { const sellStonks = (user, stonkName, quantityPhrase) => {
user.holdings ??= {} user.holdings ??= {}
user.holdings[stonkName] ??= 0 user.holdings[stonkName] ??= 0
const quantity = parseAll(quantityPhrase, user.holdings[stonkName]) const quantity = parseAll(quantityPhrase, user.holdings[stonkName], user)
if (!quantity) { if (!quantity) {
return 'Quantity must be positive integer!' return 'Quantity must be positive integer!'
} }
@ -1847,10 +2041,16 @@ command(
if (event.text.length > 64 + '!speak '.length) { if (event.text.length > 64 + '!speak '.length) {
return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`) return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
} }
if (event.text.length > 64 + '!speak '.length) {
return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
}
if (event.text.includes('<@')) {
return say(`Please don't @ people when using my voice, you're just gonna get me in trouble.`)
}
user.lastSpeech = today user.lastSpeech = today
const message = event.text.substring(7) const message = event.text.substring(7)
await slack.postToTechThermostatChannel(message) await slack.postToTechThermostatChannel(message)
//saveGame(true) //saveGame(null, true)
}, { }, {
hidden: true, hidden: true,
condition: ({ user }) => userHasCheckedQuackgrade(user, 'theVoice') condition: ({ user }) => userHasCheckedQuackgrade(user, 'theVoice')
@ -1882,7 +2082,7 @@ command(
const achName = args.join(' ') const achName = args.join(' ')
const matcher = fuzzyMatcher(achName) const matcher = fuzzyMatcher(achName)
const achievement = Object.entries(achievements) const achievement = Object.entries(achievements)
.find(([name, ach]) => matcher.test(ach.name) || matcher.test(name)) .find(([name, ach]) => [name, ach.name].some(matcher.test))
if (!achievement || !user.achievements[achievement[0]]) { if (!achievement || !user.achievements[achievement[0]]) {
return say(`You don't have any achievement matching '${achName}'`) return say(`You don't have any achievement matching '${achName}'`)
} }
@ -1896,4 +2096,19 @@ command(
{ hidden: true } { hidden: true }
) )
webapi.launch() command(
['!user-list'],
'Lists all users',
async ({ say }) => {
const users = await slack.app.client.users.list()
console.log(users.members.filter(m => m.real_name?.toLowerCase().includes('nik')))
say('k')
}, adminOnly
)
//webapi.launch()
module.exports = {
command,
adminOnly
}

View File

@ -1,4 +1,4 @@
const { setHighestCoins, addAchievement, chaosFilter, commas, saveGame, getUser } = require('./utils') const { addAchievement, saveGame, getUser } = require('./utils')
const slack = require('../../slack') const slack = require('../../slack')
let loreCount = 0 let loreCount = 0
@ -41,6 +41,7 @@ const lore = [
l(`https://i.imgur.com/eFreg7Y.gif\n`), l(`https://i.imgur.com/eFreg7Y.gif\n`),
//l(`As you might imagine, the ninth egg was I, the almighty Hvacker.`) //l(`As you might imagine, the ninth egg was I, the almighty Hvacker.`)
// In due time, the ninth egg (I, the almighty Hvacker)
] ]
slack.onReaction(async ({ event, say }) => { slack.onReaction(async ({ event, say }) => {
@ -61,7 +62,7 @@ slack.onReaction(async ({ event, say }) => {
console.log('lore:', lore[user.lore]) console.log('lore:', lore[user.lore])
await say(lore[user.lore].correctResponse) await say(lore[user.lore].correctResponse)
user.lore += 1 user.lore += 1
saveGame() saveGame(`updating ${user.name}'s lore counter`)
} catch (e) {console.error('onReaction error', e)} } catch (e) {console.error('onReaction error', e)}
}) })

View File

@ -1,5 +1,7 @@
const { commas, quackGradeMultiplier, prestigeMultiplier, makeBackup, userHasCheckedQuackgrade } = require('./utils') const { commas, quackGradeMultiplier, prestigeMultiplier, makeBackup, userHasCheckedQuackgrade, getUser } = require('./utils')
const { quackStore } = require('./quackstore') const { quackStore } = require('./quackstore')
const buyableItems = require('./buyableItems')
const slack = require('../../slack')
const possiblePrestige = coins => { const possiblePrestige = coins => {
let p = 0 let p = 0
@ -26,17 +28,27 @@ const prestigeRoute = async ({ say, args, user }) => {
'Say \'!!prestige me\' to confirm.' 'Say \'!!prestige me\' to confirm.'
) )
} else { } else {
const currentCost = totalCostForPrestige(possible)
const nextCost = totalCostForPrestige(possible + 1)
const diff = nextCost - currentCost
const progress = user.coinsAllTime - currentCost
const bars = Math.round((progress / diff) * 10)
const empty = 10 - bars
const progressBar = '[' + '='.repeat(bars) + ' '.repeat(empty) + ']'
await say( await say(
`Current Prestige: ${commas(current)}\n\n` + `Current Prestige: ${commas(current)}\n\n` +
`Quacks gained if you prestige now: ${commas(possible - current)}\n\n` + `Quacks gained if you prestige now: ${commas(possible - current)}\n\n` +
`HVAC until next quack: ${commas(totalCostForPrestige(possible + 1) - user.coinsAllTime)}\n\n` + `Next quack progress: \`${progressBar} ${commas(diff)} \`\n\n` +
'Say \'!prestige me\' to start the prestige process.' + 'Say \'!prestige me\' to start the prestige process.' +
`\n\nYour prestige is currently boosting your CPS by ${commas((prestigeMultiplier(user) - 1) * 100)}%` `\n\nYour prestige is currently boosting your CPS by ${commas((prestigeMultiplier(user) - 1) * 100)}%`
) )
} }
}//, true, adminOnly) }//, true, adminOnly)
const prestigeConfirmRoute = async ({ event, say, user }) => { const prestigeConfirmRoute = async ({ event, say, user, YEET }) => {
if (YEET) {
return say(prestigeMenu(user))
}
const possible = possiblePrestige(user.coinsAllTime) const possible = possiblePrestige(user.coinsAllTime)
const current = user.prestige const current = user.prestige
if (possible <= current) { if (possible <= current) {
@ -49,18 +61,23 @@ const prestigeConfirmRoute = async ({ event, say, user }) => {
} }
await makeBackup() await makeBackup()
user.isPrestiging = true
user.quacks ??= 0 user.quacks ??= 0
user.quacks += (possible - user.prestige) user.quacks += (possible - user.prestige)
user.prestige = possible user.prestige = possible
user.highestEver = 0 user.highestEver = 0
user.coins = 0 user.coins = 0
user.items = {}; user.items = {}
user.holdings = {}
const starterUpgrades = (user.quackUpgrades?.starter || []) const starterUpgrades = (user.quackUpgrades?.starter || [])
starterUpgrades.forEach(upgradeName => quackStore[upgradeName].effect(user)) starterUpgrades.forEach(upgradeName => quackStore[upgradeName].effect(user))
user.upgrades = {} user.upgrades = {}
await say('You prestiged! Check out !quackstore to see what you can buy!') await say(prestigeMenu(user))
await say(`Say !quack _upgrade-name_ to purchase new quackgrades!`)
//await say('You prestiged! Check out !quackstore to see what you can buy!')
} }
const quackStoreListing = (showCost = true) => ([name, upgrade]) => const quackStoreListing = (showCost = true) => ([name, upgrade]) =>
@ -75,7 +92,6 @@ const hasPreReqs = user => ([name, upgrade]) => {
return true return true
} }
const allUserUpgrades = allUserQuackUpgrades(user) const allUserUpgrades = allUserQuackUpgrades(user)
console.log('allUserUpgrades', allUserUpgrades)
return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq)) return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq))
} }
@ -93,19 +109,18 @@ const quackStoreText = user =>
`\n\nYou have ${user.quacks ??= 0} quacks to spend.` + `\n\nYou have ${user.quacks ??= 0} quacks to spend.` +
`\nQuackStore upgrades are currently boosting your CPS by ${commas((quackGradeMultiplier(user) - 1) * 100)}%` `\nQuackStore upgrades are currently boosting your CPS by ${commas((quackGradeMultiplier(user) - 1) * 100)}%`
const quackStoreRoute = async ({ user, say, args }) => { const quackStoreRoute = async ({ user, say, args, YEET }) => {
user.quackUpgrades ??= {} user.quackUpgrades ??= {}
const quacks = user.quacks ??= 0 if (!args[0] || !YEET) {
if (!args[0]) {
await say(quackStoreText(user)) await say(quackStoreText(user))
return return
} }
console.log(`Trying to buy ${args[0]}`)
const quackItem = quackStore[args[0]] const quackItem = quackStore[args[0]]
if (!quackItem || !unownedQuackItems(user).find(([name]) => name === args[0])) { if (!quackItem || !unownedQuackItems(user).find(([name]) => name === args[0])) {
await say(`'${args[0]}' is not available in the quack store!`) await say(`'${args[0]}' is not available in the quack store!`)
return return
} }
const quacks = user.quacks ??= 0
if (quackItem.cost > quacks) { if (quackItem.cost > quacks) {
await say(`${args[0]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`) await say(`${args[0]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
return return
@ -117,7 +132,151 @@ const quackStoreRoute = async ({ user, say, args }) => {
quackItem.effect(user) quackItem.effect(user)
} }
await say(`You bought ${args[0]}!`) await say(`You bought ${args[0]}!`)
//saveGame() }
const buyQuackGradeButton = quackgrade => {
//console.log('buyQuackGradeButton', quackgrade[1])
const [name, object] = quackgrade
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `:${object.emoji}: ${object.name} - ${object.cost} Quacks\n_${object.description}_`
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Buy',
emoji: true
},
value: 'click_me_123',
action_id: `buy-quackgrade-${name}`
}
}
}
const prestigeMenu = (user, extraMessage = '') => {
user.quackUpgrades ??= {}
const quacks = user.quacks ??= 0
return {
text: 'Prestige menu',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${extraMessage && extraMessage + '\n'}\n_You have ${quacks} quacks to spend._`
// text: `${extraMessage && extraMessage + '\n'}_*PRESTIGE IN PROGRESS*_\n_You have ${quacks} quacks to spend. You may ONLY spend quacks on this menu._`
}
},
// {
// type: 'section',
// text: {
// type: 'mrkdwn',
// text: '~Challenge mode~ _TODO_'
// },
// accessory: {
// type: 'static_select',
// placeholder: {
// type: 'plain_text',
// text: 'No challenge',
// emoji: true
// },
// options: [
// {
// text: {
// type: 'plain_text',
// text: 'No challenge',
// emoji: true
// },
// value: 'no-challenge'
// },
// /*
// {
// text: {
// type: 'plain_text',
// text: 'Clean Start (no prestige bonuses)',
// emoji: true
// },
// value: 'clean-start'
// }*/
// ],
// action_id: 'challenge_select-action'
// }
// },
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Available Quackgrades:*'
}
},
...unownedQuackItems(user).filter(hasPreReqs(user)).map(buyQuackGradeButton),
// {
// type: 'actions',
// elements: [
// {
// type: 'button',
// text: {
// type: 'plain_text',
// text: 'Complete Prestige',
// emoji: true
// },
// value: 'complete_prestige',
// action_id: 'complete_prestige'
// }
// ]
// }
]
}
}
const buyQuackGrade = async ({ body, ack, say, trueSay, payload }) => {
await ack()
const buying = payload.action_id.substring('buy-quackgrade-'.length)
console.log(`buyQuackGrade ${buying} clicked`)
const user = getUser(body.user.id)
// if (!user.isPrestiging) {
// console.log('You must be prestiging!')
// return say(`You must be prestiging to use this menu!`)
// }
const words = ['', buying, body.actions[0].text]
const [, ...args] = words
let extraMessage = ''
//say = async text => extraMessage = text
console.log('quackStoreRoute')
await quackStoreRoute({ say, args, user, YEET: true })
await slack.app.client.chat.update({
channel: body.channel.id,
ts: body.message.ts,
...prestigeMenu(user, extraMessage)
})
}
Object.keys(quackStore).forEach(itemName => slack.app.action('buy-quackgrade-' + itemName, buyQuackGrade))
slack.app.action('complete_prestige', async ({ body, ack, say }) => {
await ack()
const user = getUser(body.user.id)
delete user.isPrestiging
await slack.app.client.chat.delete({
channel: body.channel.id,
ts: body.message.ts,
})
await say(`Prestige complete!`)
})
const prestigeMenuRoute = async ({ say, user }) => {
user.quackUpgrades ??= {}
user.quacks ??= 0
await say(prestigeMenu(user))
} }
const ownedQuacksText = user => const ownedQuacksText = user =>
@ -137,5 +296,6 @@ module.exports = {
quackStoreRoute, quackStoreRoute,
prestigeRoute, prestigeRoute,
prestigeConfirmRoute, prestigeConfirmRoute,
prestigeMenuRoute,
ownedQuacksRoute ownedQuacksRoute
} }

View File

@ -3,7 +3,7 @@ const getRandomFromArray = array => array[Math.floor(Math.random() * array.lengt
const chaosCpsMods = [3, 2, 0.1, 1, 1.5, 1.6, 0, 1.1, 1.1, 1.26] const chaosCpsMods = [3, 2, 0.1, 1, 1.5, 1.6, 0, 1.1, 1.1, 1.26]
const chaosAvg = () => chaosCpsMods.reduce((total, next) => total + next, 0) / chaosCpsMods.length const chaosAvg = () => chaosCpsMods.reduce((total, next) => total + next, 0) / chaosCpsMods.length
//const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length) + offset) % chaosCpsMods.length] //const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length) + offset) % chaosCpsMods.length]
const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length)) % chaosCpsMods.length] const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length) + offset) % chaosCpsMods.length]
const quackStore = { const quackStore = {
ascent: { ascent: {
@ -18,7 +18,7 @@ const quackStore = {
name: 'Nuclear Fuel', name: 'Nuclear Fuel',
type: 'cps', type: 'cps',
emoji: 'atom_symbol', emoji: 'atom_symbol',
description: 'The future is now. Boosts all CPS by 20%.', description: 'The future is now, old man. Boosts all CPS by 20%.',
preReqs: ['ascent'], preReqs: ['ascent'],
effect: cps => cps * 1.2, effect: cps => cps * 1.2,
cost: 5 cost: 5
@ -70,12 +70,12 @@ const quackStore = {
type: 'starter', type: 'starter',
emoji: 'baby_symbol', emoji: 'baby_symbol',
description: 'Start each prestige with 5 mice', description: 'Start each prestige with 5 mice',
preReqs: ['dryerSheet', 'chaos'], preReqs: ['ascent'],
effect: user => { effect: user => {
user.items.mouse ??= 0 user.items.mouse ??= 0
user.items.mouse += 5 user.items.mouse += 5
}, },
cost: 5 cost: 4
}, },
silverSpoon: { silverSpoon: {
@ -88,11 +88,11 @@ const quackStore = {
user.items.accountant ??= 0 user.items.accountant ??= 0
user.items.accountant += 5 user.items.accountant += 5
}, },
cost: 10 cost: 16
}, },
oceanMan: { sharkBoy: {
name: 'Ocean Man', name: 'Shark Boy',
type: 'starter', type: 'starter',
emoji: 'ocean', emoji: 'ocean',
description: 'Start each prestige with 5 whales', description: 'Start each prestige with 5 whales',
@ -101,8 +101,73 @@ const quackStore = {
user.items.whale ??= 0 user.items.whale ??= 0
user.items.whale += 5 user.items.whale += 5
}, },
cost: 20 cost: 64
} },
superClumpingLitter: {
name: 'Super-Clumping Cat Litter',
type: 'pet',
emoji: 'smirk_cat',
description: 'Extra-strength pet effects',
preReqs: ['sharkBoy'],
effect: petMultiplier => {
petMultiplier = Math.max(petMultiplier, 1)
return petMultiplier * petMultiplier
},
cost: 128
},
magnetMan: {
name: 'Magnet Man',
type: 'starter',
emoji: 'magnet',
description: 'Start each prestige with 5 Trains',
preReqs: ['sharkBoy'],
effect: user => {
user.items.train ??= 0
user.items.train += 5
},
cost: 256
},
catFan: {
name: 'Cat Fan',
type: 'pet',
emoji: 'cat',
description: 'Super extra-strength pet effects',
preReqs: ['magnetMan', 'superClumpingLitter'],
effect: petMultiplier => {
petMultiplier = Math.max(petMultiplier, 1)
return petMultiplier * petMultiplier
},
cost: 512
},
lavaGirl: {
name: 'Lava Girl',
type: 'starter',
emoji: 'volcano',
description: 'Start each prestige with 5 Fire',
preReqs: ['magnetMan'],
effect: user => {
user.items.fire ??= 0
user.items.fire += 5
},
cost: 1024
},
aussie: {
name: 'Aussie',
type: 'starter',
emoji: 'flag-au',
description: 'Start each prestige with 5 Boomerangs',
preReqs: ['lavaGirl'],
effect: user => {
user.items.boomerang ??= 0
user.items.boomerang += 5
},
cost: 4096
},
} }
module.exports = { module.exports = {

View File

@ -1,4 +1,7 @@
const basic = ({ type, description, count, cost, extraCondition = () => true, effect = cps => cps * 2 }) => ({ const { getCPS, setUpgrades } = require('./utils');
const basic = ({ name, type, description, count, cost, extraCondition = () => true, effect = cps => cps * 2 }) => ({
name,
type, type,
description, description,
condition: (user, squadGrades) => user.items[type] >= count && extraCondition(user, squadGrades), condition: (user, squadGrades) => user.items[type] >= count && extraCondition(user, squadGrades),
@ -6,7 +9,8 @@ const basic = ({ type, description, count, cost, extraCondition = () => true, ef
effect effect
}) })
const evil = ({ type, description, cost }) => basic({ const evil = ({ name, type, description, cost }) => basic({
name,
type, type,
description, description,
count: 40, count: 40,
@ -14,7 +18,8 @@ const evil = ({ type, description, cost }) => basic({
extraCondition: (user, squadGrades) => squadGrades?.includes('discardHumanMorals'), extraCondition: (user, squadGrades) => squadGrades?.includes('discardHumanMorals'),
}) })
const heavenly = ({ type, description, cost, multiplier = 2 }) => ({ const heavenly = ({ name, type, description, cost, multiplier = 2 }) => ({
name,
type, type,
description, description,
condition: (user, squadGrades) => user.items[type] >= 60 && squadGrades?.includes('redemption'), condition: (user, squadGrades) => user.items[type] >= 60 && squadGrades?.includes('redemption'),
@ -24,7 +29,8 @@ const heavenly = ({ type, description, cost, multiplier = 2 }) => ({
const disabled = () => false const disabled = () => false
const baby = ({ type, description, cost }) => basic({ const baby = ({ name, type, description, cost }) => basic({
name,
type, type,
description, description,
count: 70, count: 70,
@ -32,7 +38,8 @@ const baby = ({ type, description, cost }) => basic({
extraCondition: disabled extraCondition: disabled
}) })
const geometry = ({ type, description, cost }) => basic({ const geometry = ({ name, type, description, cost }) => basic({
name,
type, type,
description, description,
count: 100, count: 100,
@ -40,7 +47,8 @@ const geometry = ({ type, description, cost }) => basic({
extraCondition: disabled extraCondition: disabled
}) })
const universitality = ({ type, description, cost }) => basic({ const universitality = ({ name, type, description, cost }) => basic({
name,
type, type,
description, description,
count: 100, count: 100,
@ -50,134 +58,157 @@ const universitality = ({ type, description, cost }) => basic({
module.exports = { module.exports = {
doubleClick: basic({ doubleClick: basic({
name: 'Double-Click',
type: 'mouse', type: 'mouse',
description: 'Doubles the power of mice', description: 'Doubles the power of mice',
count: 1, count: 1,
cost: 1_000 cost: 1_000
}), }),
stinkierCheese: basic({ stinkierCheese: basic({
name: 'Stinkier Cheese',
type: 'mouse', type: 'mouse',
description: 'Mice are doubly motivated to hunt down HVAC Coins', description: 'Mice are doubly motivated to hunt down HVAC Coins',
count: 10, count: 10,
cost: 21_000 cost: 21_000
}), }),
biggerTeeth: basic({ biggerTeeth: basic({
name: 'Bigger Teeth',
type: 'mouse', type: 'mouse',
description: 'Mice can intimidate twice as much HVAC out of their victims.', description: 'Mice can intimidate twice as much HVAC out of their victims.',
count: 25, count: 25,
cost: 50_000 cost: 50_000
}), }),
rats: evil({ rats: evil({
name: 'Rats',
type: 'mouse', type: 'mouse',
description: 'Consume the rotten remains of your foes', description: 'Consume the rotten remains of your foes',
cost: 150_000, cost: 150_000,
}), }),
hoodedMice: heavenly({ hoodedMice: heavenly({
name: 'Hooded Mice',
type: 'mouse', type: 'mouse',
description: 'These monks have nearly reached enlightenment. 10x Mouse CPS.', description: 'These monks have nearly reached enlightenment. 10x Mouse CPS.',
cost: 1_000_000, cost: 1_000_000,
multiplier: 10, multiplier: 10,
}), }),
babyMouse: baby({ babyMouse: baby({
name: 'Baby Mouse',
type: 'mouse', type: 'mouse',
description: 'Squeak!', description: 'Squeak!',
cost: 6_000_000, cost: 6_000_000,
}), }),
fasterComputers: basic({ fasterComputers: basic({
name: 'Faster Computers',
type: 'accountant', type: 'accountant',
description: 'Accountants can ~steal~ optimize twice as much HVAC!', description: 'Accountants can ~steal~ optimize twice as much HVAC!',
count: 1, count: 1,
cost: 11_000, cost: 11_000,
}), }),
lackOfMorality: basic({ lackOfMorality: basic({
name: 'Lack of Morality',
type: 'accountant', type: 'accountant',
description: 'Accountants are taking a hint from nearby CEOs.', description: 'Accountants are taking a hint from nearby CEOs.',
count: 10, count: 10,
cost: 200_000, cost: 200_000,
}), }),
widerBrains: basic({ widerBrains: basic({
name: 'Wider Brains',
type: 'accountant', type: 'accountant',
description: 'For accountant do double of thinking.', description: 'For accountant do double of thinking.',
count: 25, count: 25,
cost: 550_000, cost: 550_000,
}), }),
vastLayoffs: evil({ vastLayoffs: evil({
name: 'Vast Layoffs',
type: 'accountant', type: 'accountant',
description: 'The weak are not part of our future.', description: 'The weak are not part of our future.',
cost: 2_450_000, cost: 2_450_000,
}), }),
charityFund: heavenly({ charityFund: heavenly({
name: 'Charity Fund',
type: 'accountant', type: 'accountant',
description: 'THIS one is more than just a tax break. 9x Accountant CPS.', description: 'THIS one is more than just a tax break. 9x Accountant CPS.',
cost: 16_333_333, cost: 16_333_333,
multiplier: 9, multiplier: 9,
}), }),
mathBaby: baby({ mathBaby: baby({
name: 'Math Baby',
type: 'accountant', type: 'accountant',
description: '2 + 2 = WAAH!', description: '2 + 2 = WAAH!',
cost: 99_999_999, cost: 99_999_999,
}), }),
biggerBlowhole: basic({ biggerBlowhole: basic({
name: 'Bigger Blowhole',
type: 'whale', type: 'whale',
description: 'With all that extra air, whales have double power!', description: 'With all that extra air, whales have double power!',
count: 1, count: 1,
cost: 120_000 cost: 120_000
}), }),
sassyWhales: basic({ sassyWhales: basic({
name: 'Sassy Whales',
type: 'whale', type: 'whale',
description: 'These are the kind of whales that know how to get twice as much done', description: 'These are the kind of whales that know how to get twice as much done',
count: 10, count: 10,
cost: 3_000_000 cost: 3_000_000
}), }),
thinnerWater: basic({ thinnerWater: basic({
name: 'Thinner Water',
type: 'whale', type: 'whale',
description: 'Whales can move twice as quickly through this physics-defying liquid', description: 'Whales can move twice as quickly through this physics-defying liquid',
count: 25, count: 25,
cost: 6_000_000 cost: 6_000_000
}), }),
blightWhales: evil({ blightWhales: evil({
name: 'Blight Whales',
type: 'whale', type: 'whale',
description: `Infectious with evil, they swim the ocean spreading their spores.`, description: `Infectious with evil, they swim the ocean spreading their spores.`,
cost: 24_000_000 cost: 24_000_000
}), }),
whaleChoir: heavenly({ whaleChoir: heavenly({
name: 'Whale Choir',
type: 'whale', type: 'whale',
description: `Their cleansing songs reverberate through the sea. 8x Whale CPS.`, description: `Their cleansing songs reverberate through the sea. 8x Whale CPS.`,
cost: 144_000_000, cost: 144_000_000,
multiplier: 8, multiplier: 8,
}), }),
smolWhales: baby({ smolWhales: baby({
name: 'Smol Whales',
type: 'whale', type: 'whale',
description: ``, description: ``,
cost: 8_400_000_000 cost: 8_400_000_000
}), }),
greasyTracks: basic({ greasyTracks: basic({
name: 'Greasy Tracks',
type: 'train', type: 'train',
description: 'Lets trains deliver HVAC twice as efficiently', description: 'Lets trains deliver HVAC twice as efficiently',
count: 1, count: 1,
cost: 1_300_000 cost: 1_300_000
}), }),
rocketThrusters: basic({ rocketThrusters: basic({
name: 'Rocket Thrusters',
type: 'train', type: 'train',
description: 'That\'ll put some quack on your track', description: 'That\'ll put some quack on your track',
count: 10, count: 10,
cost: 22_000_000 cost: 22_000_000
}), }),
loudConductors: basic({ loudConductors: basic({
name: 'Loud Conductors',
type: 'train', type: 'train',
description: 'Conductors can onboard twice as much HVAC', description: 'Conductors can onboard twice as much HVAC',
count: 25, count: 25,
cost: 65_000_000 cost: 65_000_000
}), }),
hellTrain: evil({ hellTrain: evil({
name: 'Hell Train',
type: 'train', type: 'train',
description: 'Shipping blood needed for the ritual.', description: 'Shipping blood needed for the ritual.',
cost: 370_000_000 cost: 370_000_000
}), }),
toyTrain: heavenly({ toyTrain: heavenly({
name: 'Toy Train',
type: 'train', type: 'train',
description: 'Toot toot! 8x Train CPS.', description: 'Toot toot! 8x Train CPS.',
multiplier: 8, multiplier: 8,
@ -185,64 +216,75 @@ module.exports = {
}), }),
gasolineFire: basic({ gasolineFire: basic({
name: 'Gasoline Fire',
type: 'fire', type: 'fire',
description: 'Extremely good for breathing in.', description: 'Extremely good for breathing in.',
count: 1, count: 1,
cost: 14_000_000 cost: 14_000_000
}), }),
extremelyDryFuel: basic({ extremelyDryFuel: basic({
name: 'Extremely Dry Fuel',
type: 'fire', type: 'fire',
description: 'Hey, psst, hey. Use the ignite command for a secret achievement.', description: 'Hey, psst, hey. Use the ignite command for a secret achievement.',
count: 10, count: 10,
cost: 163_000_000 cost: 163_000_000
}), }),
cavemanFire: basic({ cavemanFire: basic({
name: 'Caveman Fire',
type: 'fire', type: 'fire',
description: 'They just don\'t make \'em like they used to.', description: 'They just don\'t make \'em like they used to.',
count: 25, count: 25,
cost: 700_000_000 cost: 700_000_000
}), }),
lava: evil({ lava: evil({
name: 'Lava',
type: 'fire', type: 'fire',
description: `Hopefully no usurpers have any "accidents".`, description: `Hopefully no usurpers have any "accidents".`,
cost: 4_200_000_000 cost: 4_200_000_000
}), }),
blueFire: heavenly({ blueFire: heavenly({
name: 'Blue Fire',
type: 'fire', type: 'fire',
description: `You can hear it singing with delight. 7x Fire CPS.`, description: `You can hear it singing with delight. 7x Fire CPS.`,
multiplier: 7, multiplier: 7,
cost: 25_200_000_000 cost: 25_200_000_000
}), }),
cuteFire: baby({ cuteFire: baby({
name: 'Cute Fire',
type: 'fire', type: 'fire',
description: `I just met my perfect match...`, description: `I just met my perfect match...`,
cost: 150_000_000_000 cost: 150_000_000_000
}), }),
spoonerang: basic({ spoonerang: basic({
name: 'Spoonerang',
type: 'boomerang', type: 'boomerang',
description: 'Scoops up HVAC mid-flight', description: 'Scoops up HVAC mid-flight',
count: 1, count: 1,
cost: 200_000_000 cost: 200_000_000
}), }),
boomerAng: basic({ boomerAng: basic({
name: 'Boomer-ang',
type: 'boomerang', type: 'boomerang',
description: 'It\'s... old.', description: 'It\'s... old.',
count: 10, count: 10,
cost: 1_200_000_000 cost: 1_200_000_000
}), }),
doubleRang: basic({ doubleRang: basic({
name: 'Double-rang',
type: 'boomerang', type: 'boomerang',
description: 'You throw one, but somehow catch two', description: 'You throw one, but somehow catch two',
count: 25, count: 25,
cost: 10_000_000_000 cost: 10_000_000_000
}), }),
loyalRang: evil({ loyalRang: evil({
name: 'Loyal-rang',
type: 'boomerang', type: 'boomerang',
description: `Frequently reports back to your throne on the state of your empire.`, description: `Frequently reports back to your throne on the state of your empire.`,
cost: 60_000_000_000 cost: 60_000_000_000
}), }),
youRang: heavenly({ youRang: heavenly({
name: 'You-rang',
type: 'boomerang', type: 'boomerang',
description: 'Your arms and legs recede into your body. You bend at the middle. You fly. And for a moment, you are free._\n_7x Boomerang CPS.', description: 'Your arms and legs recede into your body. You bend at the middle. You fly. And for a moment, you are free._\n_7x Boomerang CPS.',
multiplier: 7, multiplier: 7,
@ -250,29 +292,34 @@ module.exports = {
}), }),
lunarPower: basic({ lunarPower: basic({
name: 'Lunar Power',
type: 'moon', type: 'moon',
description: 'Out with the sol, in with the lun!', description: 'Out with the sol, in with the lun!',
count: 1, count: 1,
cost: 3_300_000_000 cost: 3_300_000_000
}), }),
womanOnTheMoon: basic({ womanOnTheMoon: basic({
name: 'Woman on the Moon',
type: 'moon', type: 'moon',
description: 'There\'s no reason for it not to be a woman!', description: 'There\'s no reason for it not to be a woman!',
count: 10, count: 10,
cost: 39_700_000_000 cost: 39_700_000_000
}), }),
doubleCraters: basic({ doubleCraters: basic({
name: 'Double-Craters',
type: 'moon', type: 'moon',
description: 'Making every side look like the dark side.', description: 'Making every side look like the dark side.',
count: 25, count: 25,
cost: 165_000_000_000 cost: 165_000_000_000
}), }),
tidalUpheaval: evil({ tidalUpheaval: evil({
name: 'Tidal Upheaval',
type: 'moon', type: 'moon',
description: `The hell with the ocean. That's valuable_ *the abstract concept of more power* _we're losing.`, description: `The hell with the ocean. That's valuable_ *the abstract concept of more power* _we're losing.`,
cost: 865_000_000_000 cost: 865_000_000_000
}), }),
newMoon: heavenly({ newMoon: heavenly({
name: 'New Moon',
type: 'moon', type: 'moon',
description: `Build a second moon to provide space for affordable housing. 6x Moon CPS.`, description: `Build a second moon to provide space for affordable housing. 6x Moon CPS.`,
multiplier: 6, multiplier: 6,
@ -280,29 +327,34 @@ module.exports = {
}), }),
glassButterfly: basic({ glassButterfly: basic({
name: 'Glass Butterfly',
type: 'butterfly', type: 'butterfly',
description: 'Not your grandma\'s universe manipulation.', description: 'Not your grandma\'s universe manipulation.',
count: 1, count: 1,
cost: 51_000_000_000 cost: 51_000_000_000
}), }),
monarchMigration: basic({ monarchMigration: basic({
name: 'Monarch Migration',
type: 'butterfly', type: 'butterfly',
description: 'This upgrade brought to you by milkweed.', description: 'This upgrade brought to you by milkweed.',
count: 10, count: 10,
cost: 870_000_000_000 cost: 870_000_000_000
}), }),
quadWing: basic({ quadWing: basic({
name: 'Quad-Wing',
type: 'butterfly', type: 'butterfly',
description: 'Sounds a lot like a trillion bees buzzing inside your head.', description: 'Sounds a lot like a trillion bees buzzing inside your head.',
count: 25, count: 25,
cost: 2_550_000_000_000 cost: 2_550_000_000_000
}), }),
venomousMoths: evil({ venomousMoths: evil({
name: 'Venomous Moths',
type: 'butterfly', type: 'butterfly',
description: 'Specifically manufactured for their horrifying brain-melt toxins.', description: 'Specifically manufactured for their horrifying brain-melt toxins.',
cost: 12_550_000_000_000 cost: 12_550_000_000_000
}), }),
quietingNectar: heavenly({ quietingNectar: heavenly({
name: 'Quieting Nectar',
type: 'butterfly', type: 'butterfly',
description: 'Calming and extra sweet. Soothes even human ails. 6x Butterfly CPS.', description: 'Calming and extra sweet. Soothes even human ails. 6x Butterfly CPS.',
multiplier: 6, multiplier: 6,
@ -310,29 +362,34 @@ module.exports = {
}), }),
silverMirror: basic({ silverMirror: basic({
name: 'Silver Mirror',
type: 'mirror', type: 'mirror',
description: 'Excellent for stabbing vampires.', description: 'Excellent for stabbing vampires.',
count: 1, count: 1,
cost: 750_000_000_000 cost: 750_000_000_000
}), }),
pocketMirror: basic({ pocketMirror: basic({
name: 'Pocket Mirror',
type: 'mirror', type: 'mirror',
description: 'Take your self-reflection on the go!', description: 'Take your self-reflection on the go!',
count: 10, count: 10,
cost: 18_000_000_000_000 cost: 18_000_000_000_000
}), }),
window: basic({ window: basic({
name: 'Window',
type: 'mirror', type: 'mirror',
description: 'Only through looking around you can you acquire the self reflection necessary to control the thermostat.', description: 'Only through looking around you can you acquire the self reflection necessary to control the thermostat.',
count: 25, count: 25,
cost: 37_500_000_000_000 cost: 37_500_000_000_000
}), }),
crackedMirror: evil({ crackedMirror: evil({
name: 'Cracked Mirror',
type: 'mirror', type: 'mirror',
description: `YOU SMILE. DO NOT FEAR, THIS IS THE FACE OF A FRIEND.`, description: `YOU SMILE. DO NOT FEAR, THIS IS THE FACE OF A FRIEND.`,
cost: 222_000_000_000_000 cost: 222_000_000_000_000
}), }),
funHouseMirror: heavenly({ funHouseMirror: heavenly({
name: 'Fun-House Mirror',
type: 'mirror', type: 'mirror',
description: `yoU LOok so siLLY IN thesE THINgs. 5X mIRror CpS.`, description: `yoU LOok so siLLY IN thesE THINgs. 5X mIRror CpS.`,
multiplier: 5, multiplier: 5,
@ -340,29 +397,34 @@ module.exports = {
}), }),
fzero: basic({ fzero: basic({
name: 'F-Zero',
type: 'quade', type: 'quade',
description: 'Brings out his competitive spirit.', description: 'Brings out his competitive spirit.',
count: 1, count: 1,
cost: 10_000_000_000_000 cost: 10_000_000_000_000
}), }),
triHumpCamel: basic({ triHumpCamel: basic({
name: 'Trimedary Camel',
type: 'quade', type: 'quade',
description: 'YEE HAW :trimedary_camel:', description: 'YEE HAW :trimedary_camel:',
count: 10, count: 10,
cost: 200_000_000_000_000 cost: 200_000_000_000_000
}), }),
adam: basic({ adam: basic({
name: 'Adam',
type: 'quade', type: 'quade',
description: 'He could probably reach the thermostat if he wanted.', description: 'He could probably reach the thermostat if he wanted.',
count: 25, count: 25,
cost: 500_000_000_000_000 cost: 500_000_000_000_000
}), }),
thatsNotQuade: evil({ thatsNotQuade: evil({
name: `That's not Quade...`,
type: 'quade', type: 'quade',
description: `The skinless face lacks even a moustache. Nevertheless, it pledges its allegiance.`, description: `The skinless face lacks even a moustache. Nevertheless, it pledges its allegiance.`,
cost: 3_000_000_000_000_000 cost: 3_000_000_000_000_000
}), }),
hannahMontanaLinux: heavenly({ hannahMontanaLinux: heavenly({
name: 'Hannah Montana Linux',
type: 'quade', type: 'quade',
description: `The patrician's choice. 4x Quade CPS.`, description: `The patrician's choice. 4x Quade CPS.`,
multiplier: 4, multiplier: 4,
@ -370,29 +432,34 @@ module.exports = {
}), }),
latestNode: basic({ latestNode: basic({
name: 'Latest Node',
type: 'hvacker', type: 'hvacker',
description: 'The old one has terrible ergonomics, tsk tsk.', description: 'The old one has terrible ergonomics, tsk tsk.',
count: 1, count: 1,
cost: 140_000_000_000_000 cost: 140_000_000_000_000
}), }),
nativeFunctions: basic({ nativeFunctions: basic({
name: 'Native Functions',
type: 'hvacker', type: 'hvacker',
description: 'Sometimes javascript just isn\'t fast enough.', description: 'Sometimes javascript just isn\'t fast enough.',
count: 10, count: 10,
cost: 3_300_000_000_000_000 cost: 3_300_000_000_000_000
}), }),
gitCommits: basic({ gitCommits: basic({
name: 'Git Commits',
type: 'hvacker', type: 'hvacker',
description: 'The heads of multiple people in a company are better than, for example, merely one head.', description: 'The heads of multiple people in a company are better than, for example, merely one head.',
count: 25, count: 25,
cost: 7_000_000_000_000_000 cost: 7_000_000_000_000_000
}), }),
undefinedBehavior: evil({ undefinedBehavior: evil({
name: 'Undefined Behavior',
type: 'hvacker', type: 'hvacker',
description: `skREEEFDS☐☐☐☐☐it's☐jwtoo☐laate☐☐☐☐☐`, description: `skREEEFDS☐☐☐☐☐it's☐jwtoo☐laate☐☐☐☐☐`,
cost: 42_000_000_000_000_000 cost: 42_000_000_000_000_000
}), }),
mutualUnderstanding: heavenly({ mutualUnderstanding: heavenly({
name: 'Mutual Understanding',
type: 'hvacker', type: 'hvacker',
description: `lol fat chance, dummy. Points for trying, though. 3x Hvacker CPS`, description: `lol fat chance, dummy. Points for trying, though. 3x Hvacker CPS`,
multiplier: 3, multiplier: 3,
@ -400,29 +467,34 @@ module.exports = {
}), }),
coffee: basic({ coffee: basic({
name: 'Coffee',
type: 'creator', type: 'creator',
description: `Didn't you know? It makes you smarter. No consequencAAAAAA`, description: `Didn't you know? It makes you smarter. No consequencAAAAAA`,
count: 1, count: 1,
cost: 1_960_000_000_000_000 cost: 1_960_000_000_000_000
}), }),
bribery: basic({ bribery: basic({
name: 'Bribery',
type: 'creator', type: 'creator',
description: `How much could he be making that a couple bucks won't get me more HVAC?`, description: `How much could he be making that a couple bucks won't get me more HVAC?`,
count: 10, count: 10,
cost: 32_300_000_000_000_000 cost: 32_300_000_000_000_000
}), }),
vim: basic({ vim: basic({
name: 'Vim',
type: 'creator', type: 'creator',
description: `*teleports behind you*`, description: `*teleports behind you*`,
count: 25, count: 25,
cost: 100_000_000_000_000_000 cost: 100_000_000_000_000_000
}), }),
regrets: evil({ regrets: evil({
name: 'Regrets',
type: 'creator', type: 'creator',
description: `HE HAS NONE. HE LAUGHS.`, description: `HE HAS NONE. HE LAUGHS.`,
cost: 600_000_000_000_000_000 cost: 600_000_000_000_000_000
}), }),
goVegan: heavenly({ goVegan: heavenly({
name: 'Go Vegan',
type: 'creator', type: 'creator',
description: `Unlock your vegan powers. 3x Creator CPS.`, description: `Unlock your vegan powers. 3x Creator CPS.`,
multiplier: 3, multiplier: 3,
@ -430,29 +502,34 @@ module.exports = {
}), }),
angelInvestors: basic({ angelInvestors: basic({
name: 'Angel Investors',
type: 'smallBusiness', type: 'smallBusiness',
description: 'Not so small NOW are we?', description: 'Not so small NOW are we?',
count: 1, count: 1,
cost: 3_140_000_000_000_000 cost: 3_140_000_000_000_000
}), }),
officeManager: basic({ officeManager: basic({
name: 'Office Manager',
type: 'smallBusiness', type: 'smallBusiness',
description: 'Sate your laborers with snacks.', description: 'Sate your laborers with snacks.',
count: 10, count: 10,
cost: 80_000_000_000_000_000 cost: 80_000_000_000_000_000
}), }),
undyingLoyalty: basic({ undyingLoyalty: basic({
name: 'Undying Loyalty',
type: 'smallBusiness', type: 'smallBusiness',
description: 'Your foolish employees bow to your every whim, regardless of salary.', description: 'Your foolish employees bow to your every whim, regardless of salary.',
count: 25, count: 25,
cost: 138_000_000_000_000_000 cost: 138_000_000_000_000_000
}), }),
deathSquad: evil({ deathSquad: evil({
name: 'Death Squad',
type: 'smallBusiness', type: 'smallBusiness',
description: `pwease don't unionize uwu :pleading_face:`, description: `pwease don't unionize uwu :pleading_face:`,
cost: 858_000_000_000_000_000 cost: 858_000_000_000_000_000
}), }),
coop: heavenly({ coop: heavenly({
name: 'Co-Op',
type: 'smallBusiness', type: 'smallBusiness',
description: `By the people, for the people. 2x smallBusiness CPS`, description: `By the people, for the people. 2x smallBusiness CPS`,
multiplier: 2, multiplier: 2,
@ -460,36 +537,77 @@ module.exports = {
}), }),
corporateBuyouts: basic({ corporateBuyouts: basic({
name: 'Corporate Buyouts',
type: 'bigBusiness', type: 'bigBusiness',
description: 'The cornerstone of any family-run business.', description: 'The cornerstone of any family-run business.',
count: 1, count: 1,
cost: 28_140_000_000_000_000 cost: 28_140_000_000_000_000
}), }),
politicalSway: basic({ politicalSway: basic({
name: 'Political Sway',
type: 'bigBusiness', type: 'bigBusiness',
description: `What's a bit of lobbying between friends?`, description: `What's a bit of lobbying between friends?`,
count: 10, count: 10,
cost: 560_000_000_000_000_000 cost: 560_000_000_000_000_000
}), }),
humanDiscontent: basic({ humanDiscontent: basic({
name: 'Human Discontent',
type: 'bigBusiness', type: 'bigBusiness',
description: 'A sad populace is a spendy populace!', description: 'A sad populace is a spendy populace!',
count: 25, count: 25,
cost: 1_372_000_000_000_000_000 cost: 1_372_000_000_000_000_000
}), }),
weJustKillPeopleNow: evil({ weJustKillPeopleNow: evil({
name: 'We Just Kill People Now',
type: 'bigBusiness', type: 'bigBusiness',
description: 'It is extremely difficult to get more evil than we already were. Nevertheless,', description: 'It is extremely difficult to get more evil than we already were. Nevertheless,',
cost: 7_072_000_000_000_000_000 cost: 7_072_000_000_000_000_000
}), }),
makePublic: heavenly({ makePublic: heavenly({
name: 'Make Public',
type: 'bigBusiness', type: 'bigBusiness',
description: `Downplay immediate profit for more long-term benefits. 2x bigBusiness CPS.`, description: `Downplay immediate profit for more long-term benefits. 2x bigBusiness CPS.`,
multiplier: 2, multiplier: 2,
cost: 42_000_000_000_000_000_000 cost: 42_000_000_000_000_000_000
}), }),
community: basic({
name: 'Community',
type: 'government',
description: `In a sense, this is really all you need.`,
count: 1,
cost: 280_140_000_000_000_000
}),
theState: basic({
name: 'The State',
type: 'government',
description: 'Congratulations, you have a monopoly on violence.',
count: 10,
cost: 5_060_000_000_000_000_000
}),
openBorders: basic({
name: 'Open Borders',
type: 'government',
description: 'Cigars, anyone?',
count: 25,
cost: 9_999_999_999_999_999_999
}),
capitalism: evil({
name: 'Capitalism',
type: 'government',
description: 'Obviously this is the ideal economy no further questions thank you.',
cost: 70_072_000_000_000_000_000
}),
socialism: heavenly({
name: 'Socialism',
type: 'government',
description: `A dictatorship of the proletariat. And thank god.`,
multiplier: 2,
cost: 690_000_000_000_000_000_000
}),
homage: { homage: {
name: 'Homage',
type: 'general', type: 'general',
description: 'The power of original ideas increases your overall CPS by 10%', description: 'The power of original ideas increases your overall CPS by 10%',
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 200, condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 200,
@ -498,11 +616,51 @@ module.exports = {
effect: (itemCps, user) => itemCps * 1.1 effect: (itemCps, user) => itemCps * 1.1
}, },
iLoveHvac: { iLoveHvac: {
name: 'iLoveHvac',
type: 'general', type: 'general',
description: 'The power of love increases your overall CPS by 10%', description: 'The power of love increases your overall CPS by 10%',
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400, condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
emoji: 'heart', emoji: 'heart',
cost: 100_000_000_000_000, cost: 100_000_000_000_000,
effect: (itemCps, user) => itemCps * 1.1 effect: (itemCps, user) => itemCps * 1.1
},
digitalPickaxe: {
name: 'Digital Pickaxe',
type: 'mining',
description: 'Break coinful digirocks into bits, increasing the power of !mine',
condition: user => user.interactions > 100,
emoji: 'pick',
cost: 100_000,
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
},
vacuum: {
name: 'Digital Vacuum',
type: 'mining',
description: 'Suck up leftover HVAC dust, greatly increasing the power of !mine',
condition: user => user.interactions > 500,
emoji: 'vacuum',
cost: 10_000_000,
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
},
mineCart: {
name: 'HVAC Mine Cart',
type: 'mining',
description: 'You\'d shine like a diamond, down in the !mine',
condition: user => user.interactions > 1500,
emoji: 'shopping_trolley',
cost: 100_000_000_000,
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
},
fpga: {
name: 'FPGA Miner',
type: 'mining',
description: 'Wait, what kind of mining is this again?',
condition: user => user.interactions > 5000,
emoji: 'floppy_disk',
cost: 1_000_000_000_000_000,
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
} }
} }
setUpgrades(module.exports)

View File

@ -2,9 +2,13 @@ const fs = require('fs')
//const jokes = require('../jokes') //const jokes = require('../jokes')
const achievements = require('./achievements') const achievements = require('./achievements')
const buyableItems = require('./buyableItems') const buyableItems = require('./buyableItems')
const upgrades = require('./upgrades')
const { quackStore, getChaos } = require('./quackstore') const { quackStore, getChaos } = require('./quackstore')
let upgrades
const setUpgrades = upg => {
upgrades = upg
}
const saveFile = 'hvacoins.json' const saveFile = 'hvacoins.json'
const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ } const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ }
@ -37,12 +41,15 @@ const chaosFilter = (num, odds, user, max = Infinity, min = -Infinity) => {
return chaosed return chaosed
} }
const parseOr = (parseable, orFunc) => { const parseOr = (parseable, fallback) => {
try { try {
if (typeof parseable === 'function') {
parseable = parseable()
}
return JSON.parse(parseable) return JSON.parse(parseable)
} catch (e) { } catch (e) {
logError(e) logError(e)
return orFunc() return fallback()
} }
} }
@ -53,13 +60,18 @@ const makeBackup = () => {
} }
let saves = 0 let saves = 0
const saveGame = (force = true) => { const saveGame = (after, force = true) => {
if (saves % 100 === 0) { if (saves % 20 === 0) {
makeBackup() makeBackup()
} }
saves += 1 saves += 1
if (force || saves % 10 === 0) { if (force || saves % 10 === 0) {
if (after) {
console.log(`SAVING GAME after ${after}`)
} else {
console.log('SAVING GAME') console.log('SAVING GAME')
}
fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2)) fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2))
} }
} }
@ -92,7 +104,7 @@ const bigNumberWords = [
['decillion', 1_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], ['nonillion', 1_000_000_000_000_000_000_000_000_000_000],
['octillion', 1_000_000_000_000_000_000_000_000_000], ['octillion', 1_000_000_000_000_000_000_000_000_000],
['septtillion', 1_000_000_000_000_000_000_000_000], ['septillion', 1_000_000_000_000_000_000_000_000],
['sextillion', 1_000_000_000_000_000_000_000], ['sextillion', 1_000_000_000_000_000_000_000],
['quintillion', 1_000_000_000_000_000_000], ['quintillion', 1_000_000_000_000_000_000],
['quadrillion', 1_000_000_000_000_000], ['quadrillion', 1_000_000_000_000_000],
@ -101,18 +113,24 @@ const bigNumberWords = [
['million', 1_000_000], ['million', 1_000_000],
] ]
const commas = (num, precise = false) => { const commas = (num, precise = false, skipWords = false) => {
num = Math.round(num) num = Math.round(num)
if (num === 1) {
return 'one'
}
const bigNum = bigNumberWords.find(([, base]) => num >= base) const bigNum = bigNumberWords.find(([, base]) => num >= base)
if (bigNum && !precise) { if (bigNum && !precise) {
const [name, base] = bigNum const [name, base] = bigNum
const nummed = (num / base).toPrecision(3) const nummed = (num / base).toPrecision(3)
if (skipWords) {
return nummed
}
return `${nummed} ${name}` return `${nummed} ${name}`
} }
return num.toLocaleString() return num.toLocaleString()
} }
const parseAll = (str, allNum) => { const parseAll = (str, allNum, user) => {
if (!str) { if (!str) {
return NaN return NaN
} }
@ -144,11 +162,21 @@ const parseAll = (str, allNum) => {
case 'one hunna': case 'one hunna':
return 100 return 100
} }
if (user && buyableItems[str]) {
return calculateCost({ itemName: str, user, quantity: 1 })
}
console.log('STR', str) console.log('STR', str)
if (str.match(/^\d+$/)) { if (str.match(/^\d+$/)) {
return parseInt(str) return parseInt(str)
} }
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+$/)) { if (str.match(/^\d+\.\d+$/)) {
return Math.round(parseFloat(str)) return Math.round(parseFloat(str))
@ -162,6 +190,16 @@ const parseAll = (str, allNum) => {
return NaN 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() const game = loadGame()
const { users, nfts, squad } = game const { users, nfts, squad } = game
@ -182,7 +220,7 @@ const addAchievement = (user, achievementName, say) => {
} }
setTimeout(async () => { setTimeout(async () => {
user.achievements[achievementName] = true user.achievements[achievementName] = true
saveGame() saveGame(`${user.name} earned ${achievementName}`)
await say(`You earned the achievement ${achievements[achievementName].name}!`) await say(`You earned the achievement ${achievements[achievementName].name}!`)
}, 500) }, 500)
} }
@ -199,22 +237,17 @@ const getIdFromName = name => {
return null; return null;
} }
const getUser = userId => { const getUser = (userId, updateCoins = false) => {
if (!users[userId]) { users[userId] ??= {}
users[userId] = { users[userId].coins ??= 0
coins: 0,
items: {},
upgrades: {},
achievements: {},
coinsAllTime: 0,
prestige: 0
}
} else {
users[userId].items ??= {} users[userId].items ??= {}
users[userId].upgrades ??= {} users[userId].upgrades ??= {}
users[userId].achievements ??= {} users[userId].achievements ??= {}
users[userId].coinsAllTime ??= users[userId].coins users[userId].coinsAllTime ??= users[userId].coins
users[userId].prestige ??= 0 users[userId].prestige ??= 0
users[userId].startDate ??= new Date()
if (updateCoins) {
users[userId].coins = getCoins(userId)
} }
return users[userId] return users[userId]
} }
@ -251,6 +284,7 @@ const squadUpgrades = {
tastyKeyboards: { tastyKeyboards: {
name: 'Tasty Keyboards', name: 'Tasty Keyboards',
description: 'Delicious and sticky. Boosts CPS by 20% for everyone.', description: 'Delicious and sticky. Boosts CPS by 20% for everyone.',
effect: cps => cps * 1.2, effect: cps => cps * 1.2,
cost: 10_000_000_000_000, cost: 10_000_000_000_000,
emoji: 'keyboard' emoji: 'keyboard'
@ -298,6 +332,11 @@ const quackGradeMultiplier = user => {
return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1) 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 singleItemCps = (user, itemName) => {
const baseCps = buyableItems[itemName].earning const baseCps = buyableItems[itemName].earning
// console.log('') // console.log('')
@ -326,6 +365,9 @@ const singleItemCps = (user, itemName) => {
const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1) const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1)
// console.log('squadGradeMultiplier', squadGradeMultiplier) // console.log('squadGradeMultiplier', squadGradeMultiplier)
const petMultiplier = petQuackGradeMultiplier(user)
//console.log('petMultiplier', petMultiplier)
const total = const total =
baseCps * baseCps *
achievementMultiplier * achievementMultiplier *
@ -333,7 +375,8 @@ const singleItemCps = (user, itemName) => {
generalUpgradeCps * generalUpgradeCps *
quackGrade * quackGrade *
pMult * pMult *
squadGradeMultiplier squadGradeMultiplier *
petMultiplier
// console.log('Single Item CPS:', total) // console.log('Single Item CPS:', total)
@ -372,6 +415,7 @@ const definitelyShuffle = (str, percentOdds) => {
let shuffled = str let shuffled = str
while (shuffled === str) { while (shuffled === str) {
shuffled = shufflePercent(str, percentOdds) shuffled = shufflePercent(str, percentOdds)
console.log('Shuffling... "' + shuffled + '"')
} }
return shuffled return shuffled
} }
@ -436,6 +480,71 @@ game.stonkMarket ??= {
const userHasCheckedQuackgrade = (user, quackGrade) => (user.quackUpgrades?.checked || []).includes(quackGrade) 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
}
module.exports = { module.exports = {
saveGame, saveGame,
makeBackup, makeBackup,
@ -468,5 +577,10 @@ module.exports = {
userHasCheckedQuackgrade, userHasCheckedQuackgrade,
fuzzyMatcher, fuzzyMatcher,
addCoins, addCoins,
setKnownUsers: users => knownUsers = users calculateCost,
setKnownUsers: users => knownUsers = users,
petBoost,
updateAll,
setSlackAppClientChatUpdate: update => slackAppClientChatUpdate = update,
setUpgrades
} }

View File

@ -61,7 +61,7 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
const user = getUser(event.user) const user = getUser(event.user)
const haunted = false const haunted = false
//await action({ event, say, words, args, commandName }) //await action({ event, say, words, args, commandName })
const canUse = await condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Sage) }) const canUse = await condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Admin) })
if (!canUse) { if (!canUse) {
await say(`Command '${words[0]}' not found`) await say(`Command '${words[0]}' not found`)
return return

View File

@ -1,45 +1,63 @@
const slack = require('../slack') const slack = require('../slack')
const { updateAll } = require('../games/hvacoins/utils')
const tie = 'TIE' const tie = 'TIE'
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) => const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2, channelMap }) =>
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' + gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2], channelMap) + '\n' +
'```' + textFromBoard(board) + '\n```' '```' + textFromBoard(board) + '\n```'
const addChoiceEmojis = async ({ choices, channel, ts }) => { const addChoiceEmojis = async ({ choices, channel, ts }) => {
const addEmoji = async emojiName => const addEmoji = async emojiName => {
try {
await slack.app.client.reactions.add({ await slack.app.client.reactions.add({
channel, channel,
timestamp: ts, timestamp: ts,
name: emojiName name: emojiName
}) })
} catch (ignore) {
}
}
for (const choice of choices) { for (const choice of choices) {
await addEmoji(choice) await addEmoji(choice)
} }
} }
const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => { const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => {
if (event.channel_type === 'im') { if (event.channel_type !== 'im') {
return;
}
const eventText = event.text?.toLowerCase() const eventText = event.text?.toLowerCase()
if (eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword))) { if (!(eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword)))) {
return;
}
try {
console.log('Trigger found') console.log('Trigger found')
const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0] const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0]
console.log('Messaging opponent ' + slack.users[opponent.substring(2, opponent.length - 1)]) console.log('Messaging opponent ' + slack.users[opponent.substring(2, opponent.length - 1)])
const msg = messageFromBoard({ const channelMap = {}
const msg = () => messageFromBoard({
dataName, dataName,
gameName, gameName,
textFromBoard, textFromBoard,
board: initialBoard(), board: initialBoard(),
player1: '<@' + event.user + '>', player1: '<@' + event.user + '>',
player2: opponent player2: opponent,
channelMap
}) })
const sent = await say(msg) const sent = await say(msg())
await addChoiceEmojis({ ...sent, choices: turnChoiceEmojis }) channelMap[event.user] = {
channel: sent.channel,
ts: sent.ts
} }
await updateAll({ name: gameName, text: msg(), add: sent })
await addChoiceEmojis({...sent, choices: turnChoiceEmojis})
} catch (e) {
console.error(e)
} }
} }
const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, { board, players }) const encodeGame = (dataKey, board, players, channelMap = {}) => slack.encodeData(dataKey, { board, players, channelMap })
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message) const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
@ -68,7 +86,8 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
return return
} }
const { board, players } = game game.channelMap ??= {}
const { board, players, channelMap } = game
let winner = checkWinner(board) let winner = checkWinner(board)
if (winner) { if (winner) {
console.log('winner found: ' + winner) console.log('winner found: ' + winner)
@ -85,33 +104,58 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
} }
winner = checkWinner(board) winner = checkWinner(board)
const boardMessage = messageFromBoard({ const boardMessage = () => messageFromBoard({
dataName, dataName,
gameName, gameName,
textFromBoard, textFromBoard,
board, board,
player1, player1,
player2 player2,
channelMap
}) })
const winnerMessages = getMessages(winner) if (winner) {
await say(boardMessage + winnerMessages.you) await updateAll({ name: gameName, text: boardMessage() + '\nSomebody won! I do not yet know who!' })
if (!winner) { const removeEmoji = emojiName => Object.values(channelMap).forEach(({ channel, ts }) =>
await say('Waiting for opponent\'s response...')
}
const removeEmoji = async emojiName =>
slack.app.client.reactions.remove({ slack.app.client.reactions.remove({
channel: event.item.channel, channel,
timestamp: message.messages[0]?.ts, timestamp: ts,
name: emojiName name: emojiName
}) }))
turnChoiceEmojis.forEach(removeEmoji) turnChoiceEmojis.forEach(removeEmoji)
return
}
const winnerMessages = getMessages(winner)
// await say(boardMessage() + winnerMessages.you)
console.log('TurnHandler', { gameName, boardMessage: boardMessage() })
// await updateAll({ name: gameName, text: boardMessage() + '\nTurnHandler' })
// if (!winner) {
// await say('Waiting for opponent\'s response...')
// }
// const removeEmoji = async emojiName =>
// slack.app.client.reactions.remove({
// channel: event.item.channel,
// timestamp: message.messages[0]?.ts,
// name: emojiName
// })
// turnChoiceEmojis.forEach(removeEmoji)
console.log('SENDING to ' + opponent) console.log('SENDING to ' + opponent)
if (!channelMap[opponent]) {
const sentBoard = await slack.app.client.chat.postMessage({ const sentBoard = await slack.app.client.chat.postMessage({
channel: opponent, channel: opponent,
text: boardMessage + winnerMessages.opponent text: boardMessage() + winnerMessages.opponent
}) })
channelMap[opponent] = {
channel: sentBoard.channel,
ts: sentBoard.ts
}
console.log('BOARD MESSAGE AFTER ')
await updateAll({ name: gameName, text: boardMessage(), add: sentBoard })
} else {
await updateAll({ name: gameName, text: boardMessage() })
}
if (!winner) { if (!winner) {
const sentBoard = channelMap[opponent]
await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis }) await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis })
} }
} }

View File

@ -1,19 +1,22 @@
const { App: SlackApp } = require('@slack/bolt') const { App: SlackApp } = require('@slack/bolt')
const config = require('../config') const config = require('../config')
const { addReactions, saveGame } = require('../games/hvacoins/utils') const fs = require('fs')
const { addReactions, saveGame, setSlackAppClientChatUpdate, parseOr } = require('../games/hvacoins/utils')
const temperatureChannelId = 'C034156CE03' const temperatureChannelId = 'C034156CE03'
const dailyStandupChannelId = 'C03L533AU3Z'
const pollingMinutes = 5 const pollingMinutes = 5
const pollingPeriod = 1000 * 60 * pollingMinutes const pollingPeriod = 1000 * 60 * pollingMinutes
const MAX_POLLS = 3
const HOURS_PER_WINDOW = 2
const colderEmoji = 'snowflake' const colderEmoji = 'snowflake'
const hotterEmoji = 'fire' const hotterEmoji = 'fire'
const goodEmoji = '+1' const goodEmoji = '+1'
let app const app = new SlackApp({
try {
app = new SlackApp({
token: config.slackBotToken, token: config.slackBotToken,
signingSecret: config.slackSigningSecret, signingSecret: config.slackSigningSecret,
appToken: config.slackAppToken, appToken: config.slackAppToken,
@ -23,9 +26,6 @@ try {
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id // temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
// console.log('techThermostatChannelId', temperatureChannelId) // console.log('techThermostatChannelId', temperatureChannelId)
// }) // })
} catch (e) {
console.log('Failed to initialize SlackApp', e)
}
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater'] const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater']
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>'] const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
@ -41,7 +41,7 @@ const sendHelp = async (say, prefix) => {
text: prefix + text: prefix +
`Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` + `Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` +
'\'Hotter\' and \'Colder\' votes offset. E.g. with votes Hotter - 4, Colder - 3, and Content - 2, the temp won\'t change.\n' + '\'Hotter\' and \'Colder\' votes offset. E.g. with votes Hotter - 4, Colder - 3, and Content - 2, the temp won\'t change.\n' +
'At this time I am not capable of actually changing the temperature. Go bug Quade.' 'At this time I am not capable of actually changing the temperature. Go bug Michael.'
}) })
} }
@ -59,34 +59,8 @@ app.event('reaction_added', async ({ event, context, client, say }) => {
} }
}) })
const users = { const users = parseOr(fs.readFileSync('./users.json', 'utf-8'),
U028BMEBWBV: 'Sage', () => ({}))
U02U15RFK4Y: 'Adam',
U02AAB54V34: 'Houston',
U02KYLVK1GV: 'Quade',
U017PG4EL1Y: 'Max',
UTDLFGZA5: 'Tyler',
U017CB5L1K3: 'Andres',
U0344TFA7HQ: 'Hvacker',
U0X0ZQCN6: 'Caleb',
U03BBTD4CQZ: 'Fernando',
U03DF152WUV: 'Nik',
U2X0SG7BP: 'John',
UR2H5KNHY: 'Jake',
Sage: 'U028BMEBWBV',
Adam: 'U02U15RFK4Y',
Houston: 'U02AAB54V34',
Quade: 'U02KYLVK1GV',
Max: 'U017PG4EL1Y',
Tyler: 'UTDLFGZA5',
Andres: 'U017CB5L1K3',
Caleb: 'U0X0ZQCN6',
Hvacker: 'U0344TFA7HQ',
Fernando: 'U03BBTD4CQZ',
John: 'U2X0SG7BP',
Jake: 'UR2H5KNHY',
}
const buildSayPrepend = ({ say, prepend }) => async msg => { const buildSayPrepend = ({ say, prepend }) => async msg => {
if (typeof(msg) === 'string') { if (typeof(msg) === 'string') {
@ -99,10 +73,11 @@ const buildSayPrepend = ({ say, prepend }) => async msg => {
} }
process.once('SIGINT', code => { process.once('SIGINT', code => {
saveGame(true) saveGame(null, true)
process.exit() process.exit()
}) })
let pollHistory = []
const activePolls = {} const activePolls = {}
const testId = 'U028BMEBWBV_TEST' const testId = 'U028BMEBWBV_TEST'
let testMode = false let testMode = false
@ -113,18 +88,18 @@ app.event('message', async ({ event, context, client, say }) => {
userName: users[event.user] userName: users[event.user]
}) })
} }
if (event?.user === users.Sage) { if (event?.user === users.Admin) {
if (event?.text.startsWith('!')) { if (event?.text.startsWith('!')) {
if (testMode) { if (testMode) {
await messageSage('Currently in test mode!') await messageAdmin('Currently in test mode!')
} }
} }
if (event?.text === '!test') { if (event?.text === '!test') {
testMode = !testMode testMode = !testMode
await messageSage(`TestMode: ${testMode} with ID ${testId}`) await messageAdmin(`TestMode: ${testMode} with ID ${testId}`)
} else if (event?.text === '!notest') { } else if (event?.text === '!notest') {
testMode = false testMode = false
await messageSage(`TestMode: ${testMode}`) await messageAdmin(`TestMode: ${testMode}`)
} }
if (testMode) { if (testMode) {
event.user = testId event.user = testId
@ -136,16 +111,21 @@ app.event('message', async ({ event, context, client, say }) => {
if (event.user) { if (event.user) {
console.log('MSG', users[event.user], "'" + event.text + "'", new Date().toLocaleTimeString()) console.log('MSG', users[event.user], "'" + event.text + "'", new Date().toLocaleTimeString())
} }
if (event.user === users.Sage && event.channel === 'D0347Q4H9FE') { if (event.user === users.Admin && event.channel === 'D0347Q4H9FE') {
if (event.text === '!!kill') { if (event.text === '!!kill') {
saveGame(true) saveGame(null, true)
process.exit(1) process.exit(1)
} else if (event.text === '!!restart') { } else if (event.text === '!!restart') {
saveGame(true) if (Object.entries(activePolls).length === 0) {
process.exit() saveGame(null, true)
process.exit(0)
} else {
await messageAdmin('Restart pending poll completion...')
pendingRestart = true
}
} }
if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) { if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) {
await postToTechThermostatChannel(event.text.substring(4).trim()) await postToTechThermostatChannel(event.text.substring(4).trim().replace('@here', '<!here>'))
return return
} }
} }
@ -156,17 +136,37 @@ app.event('message', async ({ event, context, client, say }) => {
return return
} }
if (!pollTriggers.includes(eventText)) { if (!pollTriggers.includes(eventText) || event.user === users.John) {
if (halfTriggers.includes(eventText)) { if (halfTriggers.includes(eventText)) {
await sendHelp(say, 'It looks like you might want to change the temperature.') await sendHelp(say, 'It looks like you might want to change the temperature.')
} }
return return
} }
if (event.channel !== temperatureChannelId) {
return say(`Please request polls in the appropriate channel.`)
}
if (activePolls[event.channel]) { if (activePolls[event.channel]) {
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" }) await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
return return
} }
const now = new Date()
const windowStart = new Date()
windowStart.setHours(now.getHours() - HOURS_PER_WINDOW)
const pollsInWindow = pollHistory.filter(pollTime => pollTime > windowStart)
const pollText = MAX_POLLS === 1 ? 'poll' : 'polls'
const hourText = HOURS_PER_WINDOW === 1 ? 'hour' : 'hours'
if (pollsInWindow.length >= MAX_POLLS) {
await postToTechThermostatChannel({ text: `You have exceeded the limit of ${MAX_POLLS} ${pollText} per ${HOURS_PER_WINDOW} ${hourText}!` })
return
}
if (pollHistory.push(now) > MAX_POLLS) {
[, ...pollHistory] = pollHistory
}
activePolls[event.channel] = true activePolls[event.channel] = true
const pollTs = await startPoll() const pollTs = await startPoll()
@ -215,11 +215,11 @@ app.event('message', async ({ event, context, client, say }) => {
let text let text
if (hotterVotes > colderVotes && hotterVotes > contentVotes) { if (hotterVotes > colderVotes && hotterVotes > contentVotes) {
text = `<@${users.Adam}> The people have spoken, and would like to ` text = `<@${users.Michael}> The people have spoken, and would like to `
text += 'raise the temperature, quack.' text += 'raise the temperature, quack.'
requestTempChange('Hotter') requestTempChange('Hotter')
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) { } else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
text = `<@${users.Adam}> The people have spoken, and would like to ` text = `<@${users.Michael}> The people have spoken, and would like to `
text += 'lower the temperature, quack quack.' text += 'lower the temperature, quack quack.'
requestTempChange('Colder') requestTempChange('Colder')
} else { } else {
@ -230,11 +230,18 @@ app.event('message', async ({ event, context, client, say }) => {
await postToTechThermostatChannel({ text }) await postToTechThermostatChannel({ text })
delete activePolls[event.channel] delete activePolls[event.channel]
if (pendingRestart && Object.entries(activePolls).length === 0) {
await messageAdmin('Performing pending restart!')
saveGame(null, true)
process.exit(0)
}
}, pollingPeriod) }, pollingPeriod)
}) })
let pendingRestart = false
;(async () => { ;(async () => {
await app.start().catch(console.error) await app.start()
console.log('Slack Bolt has started') console.log('Slack Bolt has started')
})() })()
@ -247,7 +254,7 @@ const postToTechThermostatChannel = async optionsOrText => {
return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId }) return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId })
} }
const messageSage = async optionsOrText => messageIn(users.Sage, optionsOrText) const messageAdmin = async optionsOrText => messageIn(users.Admin, optionsOrText)
const messageIn = async (channel, optionsOrText) => { const messageIn = async (channel, optionsOrText) => {
if (optionsOrText === null || typeof optionsOrText !== 'object') { if (optionsOrText === null || typeof optionsOrText !== 'object') {
@ -262,7 +269,7 @@ const startPoll = async () => {
const sent = await postToTechThermostatChannel({ const sent = await postToTechThermostatChannel({
text: `<!here> Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted.\n` + text: `<!here> 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.` + `Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is.` +
'\n(Note that I can\'t actually change the temperature yet. Make Quade do it!)' '\n(Note that I can\'t actually change the temperature yet. Make Michael do it!)'
}) })
await addReactions({ await addReactions({
app, app,
@ -281,10 +288,12 @@ const requestTempChange = change => {
tempChangeListeners.forEach(listener => listener(change)) tempChangeListeners.forEach(listener => listener(change))
} }
// noinspection HttpUrlsUsage
const encodeData = (key, data) => const encodeData = (key, data) =>
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >` `<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
const decodeData = (key, message) => { const decodeData = (key, message) => {
try {
const regex = new RegExp(`http://${key}ZZZ[^|]*`) const regex = new RegExp(`http://${key}ZZZ[^|]*`)
let match = message.match(regex) let match = message.match(regex)
if (!match) { if (!match) {
@ -292,14 +301,31 @@ const decodeData = (key, message) => {
} }
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8')) return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
} catch (e) {
console.error(e)
return null
}
} }
const onReaction = listener => reactionListeners.push(listener) const onReaction = listener => reactionListeners.push(listener)
const channelIsIm = async channel => (await app.client.conversations.info({ channel }))?.channel?.is_im const channelIsIm = async channel => (await app.client.conversations.info({ channel }))?.channel?.is_im
const wasMyMessage = async event => {
const text = (await app.client.conversations.history({
channel: event.item.channel,
latest: event.item.ts,
limit: 1,
inclusive: true
})).messages[0].text
const decoded = decodeData('commandPayload', text)
return decoded.event.user === event.user
}
onReaction(async ({ event }) => { onReaction(async ({ event }) => {
if (event.reaction === 'x' && (event.user === users.Sage || await channelIsIm(event.item.channel))) { console.log({ event })
if (event.reaction === 'x' && (event.user === users.Admin || (await wasMyMessage(event)) || await channelIsIm(event.item.channel))) {
try { try {
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts }) await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
} catch (e) { } catch (e) {
@ -307,9 +333,12 @@ onReaction(async ({ event }) => {
} }
}) })
setSlackAppClientChatUpdate(app.client.chat.update)
module.exports = { module.exports = {
app, app,
temperatureChannelId, temperatureChannelId,
dailyStandupChannelId,
onAction: app.action, onAction: app.action,
getMessage, getMessage,
updateMessage: app.client.chat.update, updateMessage: app.client.chat.update,
@ -319,10 +348,12 @@ module.exports = {
onReaction, onReaction,
encodeData, encodeData,
decodeData, decodeData,
messageSage, messageAdmin,
messageIn, messageIn,
testMode, testMode,
testId, testId,
users, users,
buildSayPrepend buildSayPrepend,
pollTriggers,
pendingRestart
} }