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 => {
for (const row of board) {
for (const col of row) {
if (col !== ' ') {
if (col === ' ') {
return null
}
}

View File

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

View File

@ -1,16 +1,8 @@
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 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 leaderboardUpdater = {}
const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => {
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 ({
text: buyableText(highestCoins, user),
blocks: Object.entries(buyableItems)
text: (extraMessage && extraMessage + '\n')
+ `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))
.map(([itemName]) => {
const cost = calculateCost({ itemName, user, quantity: 1 })
const cps = Math.round(singleItemCps(user, itemName))
return ({ user, itemName, cost, cps })
}).map(buildBlock)
]
})
}
@ -136,7 +140,7 @@ const buyRoute = async ({ event, say, args, user }) => {
return say(`Buying ${quantity} ${buyableName} would cost you ${commas(realCost)} HVAC`)
}
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
}
user.coins -= realCost
@ -161,15 +165,20 @@ const buyButton = async ({ body, ack, say, payload }) => {
const user = getUser(event.user)
const words = ['', buying, body.actions[0].text]
const [commandName, ...args] = words
let extraMessage = ''
say = async text => extraMessage = text
await buyRoute({ event, say, words, args, commandName, user })
const highestCoins = user.highestEver || user.coins || 1
await slack.app.client.chat.update({
channel: body.channel.id,
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))
module.exports = buyRoute
module.exports = { buyRoute, leaderboardUpdater }

View File

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

File diff suppressed because it is too large Load Diff

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')
let loreCount = 0
@ -41,6 +41,7 @@ const lore = [
l(`https://i.imgur.com/eFreg7Y.gif\n`),
//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 }) => {
@ -61,7 +62,7 @@ slack.onReaction(async ({ event, say }) => {
console.log('lore:', lore[user.lore])
await say(lore[user.lore].correctResponse)
user.lore += 1
saveGame()
saveGame(`updating ${user.name}'s lore counter`)
} 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 buyableItems = require('./buyableItems')
const slack = require('../../slack')
const possiblePrestige = coins => {
let p = 0
@ -26,17 +28,27 @@ const prestigeRoute = async ({ say, args, user }) => {
'Say \'!!prestige me\' to confirm.'
)
} 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(
`Current Prestige: ${commas(current)}\n\n` +
`Quacks gained if you prestige now: ${commas(possible - current)}\n\n` +
`HVAC until next quack: ${commas(totalCostForPrestige(possible + 1) - user.coinsAllTime)}\n\n` +
`Next quack progress: \`${progressBar} ${commas(diff)} \`\n\n` +
'Say \'!prestige me\' to start the prestige process.' +
`\n\nYour prestige is currently boosting your CPS by ${commas((prestigeMultiplier(user) - 1) * 100)}%`
)
}
}//, true, adminOnly)
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 current = user.prestige
if (possible <= current) {
@ -49,18 +61,23 @@ const prestigeConfirmRoute = async ({ event, say, user }) => {
}
await makeBackup()
user.isPrestiging = true
user.quacks ??= 0
user.quacks += (possible - user.prestige)
user.prestige = possible
user.highestEver = 0
user.coins = 0
user.items = {};
user.items = {}
user.holdings = {}
const starterUpgrades = (user.quackUpgrades?.starter || [])
starterUpgrades.forEach(upgradeName => quackStore[upgradeName].effect(user))
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]) =>
@ -75,7 +92,6 @@ const hasPreReqs = user => ([name, upgrade]) => {
return true
}
const allUserUpgrades = allUserQuackUpgrades(user)
console.log('allUserUpgrades', allUserUpgrades)
return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq))
}
@ -93,19 +109,18 @@ const quackStoreText = user =>
`\n\nYou have ${user.quacks ??= 0} quacks to spend.` +
`\nQuackStore upgrades are currently boosting your CPS by ${commas((quackGradeMultiplier(user) - 1) * 100)}%`
const quackStoreRoute = async ({ user, say, args }) => {
const quackStoreRoute = async ({ user, say, args, YEET }) => {
user.quackUpgrades ??= {}
const quacks = user.quacks ??= 0
if (!args[0]) {
if (!args[0] || !YEET) {
await say(quackStoreText(user))
return
}
console.log(`Trying to buy ${args[0]}`)
const quackItem = quackStore[args[0]]
if (!quackItem || !unownedQuackItems(user).find(([name]) => name === args[0])) {
await say(`'${args[0]}' is not available in the quack store!`)
return
}
const quacks = user.quacks ??= 0
if (quackItem.cost > quacks) {
await say(`${args[0]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
return
@ -117,7 +132,151 @@ const quackStoreRoute = async ({ user, say, args }) => {
quackItem.effect(user)
}
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 =>
@ -137,5 +296,6 @@ module.exports = {
quackStoreRoute,
prestigeRoute,
prestigeConfirmRoute,
prestigeMenuRoute,
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 chaosAvg = () => chaosCpsMods.reduce((total, next) => total + next, 0) / chaosCpsMods.length
//const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length) + offset) % chaosCpsMods.length]
const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length)) % chaosCpsMods.length]
const getChaos = offset => chaosCpsMods[(Math.floor(new Date().getSeconds() / chaosCpsMods.length) + offset) % chaosCpsMods.length]
const quackStore = {
ascent: {
@ -18,7 +18,7 @@ const quackStore = {
name: 'Nuclear Fuel',
type: 'cps',
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'],
effect: cps => cps * 1.2,
cost: 5
@ -70,12 +70,12 @@ const quackStore = {
type: 'starter',
emoji: 'baby_symbol',
description: 'Start each prestige with 5 mice',
preReqs: ['dryerSheet', 'chaos'],
preReqs: ['ascent'],
effect: user => {
user.items.mouse ??= 0
user.items.mouse += 5
},
cost: 5
cost: 4
},
silverSpoon: {
@ -88,11 +88,11 @@ const quackStore = {
user.items.accountant ??= 0
user.items.accountant += 5
},
cost: 10
cost: 16
},
oceanMan: {
name: 'Ocean Man',
sharkBoy: {
name: 'Shark Boy',
type: 'starter',
emoji: 'ocean',
description: 'Start each prestige with 5 whales',
@ -101,8 +101,73 @@ const quackStore = {
user.items.whale ??= 0
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 = {

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,
description,
condition: (user, squadGrades) => user.items[type] >= count && extraCondition(user, squadGrades),
@ -6,7 +9,8 @@ const basic = ({ type, description, count, cost, extraCondition = () => true, ef
effect
})
const evil = ({ type, description, cost }) => basic({
const evil = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 40,
@ -14,7 +18,8 @@ const evil = ({ type, description, cost }) => basic({
extraCondition: (user, squadGrades) => squadGrades?.includes('discardHumanMorals'),
})
const heavenly = ({ type, description, cost, multiplier = 2 }) => ({
const heavenly = ({ name, type, description, cost, multiplier = 2 }) => ({
name,
type,
description,
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 baby = ({ type, description, cost }) => basic({
const baby = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 70,
@ -32,7 +38,8 @@ const baby = ({ type, description, cost }) => basic({
extraCondition: disabled
})
const geometry = ({ type, description, cost }) => basic({
const geometry = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 100,
@ -40,7 +47,8 @@ const geometry = ({ type, description, cost }) => basic({
extraCondition: disabled
})
const universitality = ({ type, description, cost }) => basic({
const universitality = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 100,
@ -50,134 +58,157 @@ const universitality = ({ type, description, cost }) => basic({
module.exports = {
doubleClick: basic({
name: 'Double-Click',
type: 'mouse',
description: 'Doubles the power of mice',
count: 1,
cost: 1_000
}),
stinkierCheese: basic({
name: 'Stinkier Cheese',
type: 'mouse',
description: 'Mice are doubly motivated to hunt down HVAC Coins',
count: 10,
cost: 21_000
}),
biggerTeeth: basic({
name: 'Bigger Teeth',
type: 'mouse',
description: 'Mice can intimidate twice as much HVAC out of their victims.',
count: 25,
cost: 50_000
}),
rats: evil({
name: 'Rats',
type: 'mouse',
description: 'Consume the rotten remains of your foes',
cost: 150_000,
}),
hoodedMice: heavenly({
name: 'Hooded Mice',
type: 'mouse',
description: 'These monks have nearly reached enlightenment. 10x Mouse CPS.',
cost: 1_000_000,
multiplier: 10,
}),
babyMouse: baby({
name: 'Baby Mouse',
type: 'mouse',
description: 'Squeak!',
cost: 6_000_000,
}),
fasterComputers: basic({
name: 'Faster Computers',
type: 'accountant',
description: 'Accountants can ~steal~ optimize twice as much HVAC!',
count: 1,
cost: 11_000,
}),
lackOfMorality: basic({
name: 'Lack of Morality',
type: 'accountant',
description: 'Accountants are taking a hint from nearby CEOs.',
count: 10,
cost: 200_000,
}),
widerBrains: basic({
name: 'Wider Brains',
type: 'accountant',
description: 'For accountant do double of thinking.',
count: 25,
cost: 550_000,
}),
vastLayoffs: evil({
name: 'Vast Layoffs',
type: 'accountant',
description: 'The weak are not part of our future.',
cost: 2_450_000,
}),
charityFund: heavenly({
name: 'Charity Fund',
type: 'accountant',
description: 'THIS one is more than just a tax break. 9x Accountant CPS.',
cost: 16_333_333,
multiplier: 9,
}),
mathBaby: baby({
name: 'Math Baby',
type: 'accountant',
description: '2 + 2 = WAAH!',
cost: 99_999_999,
}),
biggerBlowhole: basic({
name: 'Bigger Blowhole',
type: 'whale',
description: 'With all that extra air, whales have double power!',
count: 1,
cost: 120_000
}),
sassyWhales: basic({
name: 'Sassy Whales',
type: 'whale',
description: 'These are the kind of whales that know how to get twice as much done',
count: 10,
cost: 3_000_000
}),
thinnerWater: basic({
name: 'Thinner Water',
type: 'whale',
description: 'Whales can move twice as quickly through this physics-defying liquid',
count: 25,
cost: 6_000_000
}),
blightWhales: evil({
name: 'Blight Whales',
type: 'whale',
description: `Infectious with evil, they swim the ocean spreading their spores.`,
cost: 24_000_000
}),
whaleChoir: heavenly({
name: 'Whale Choir',
type: 'whale',
description: `Their cleansing songs reverberate through the sea. 8x Whale CPS.`,
cost: 144_000_000,
multiplier: 8,
}),
smolWhales: baby({
name: 'Smol Whales',
type: 'whale',
description: ``,
cost: 8_400_000_000
}),
greasyTracks: basic({
name: 'Greasy Tracks',
type: 'train',
description: 'Lets trains deliver HVAC twice as efficiently',
count: 1,
cost: 1_300_000
}),
rocketThrusters: basic({
name: 'Rocket Thrusters',
type: 'train',
description: 'That\'ll put some quack on your track',
count: 10,
cost: 22_000_000
}),
loudConductors: basic({
name: 'Loud Conductors',
type: 'train',
description: 'Conductors can onboard twice as much HVAC',
count: 25,
cost: 65_000_000
}),
hellTrain: evil({
name: 'Hell Train',
type: 'train',
description: 'Shipping blood needed for the ritual.',
cost: 370_000_000
}),
toyTrain: heavenly({
name: 'Toy Train',
type: 'train',
description: 'Toot toot! 8x Train CPS.',
multiplier: 8,
@ -185,64 +216,75 @@ module.exports = {
}),
gasolineFire: basic({
name: 'Gasoline Fire',
type: 'fire',
description: 'Extremely good for breathing in.',
count: 1,
cost: 14_000_000
}),
extremelyDryFuel: basic({
name: 'Extremely Dry Fuel',
type: 'fire',
description: 'Hey, psst, hey. Use the ignite command for a secret achievement.',
count: 10,
cost: 163_000_000
}),
cavemanFire: basic({
name: 'Caveman Fire',
type: 'fire',
description: 'They just don\'t make \'em like they used to.',
count: 25,
cost: 700_000_000
}),
lava: evil({
name: 'Lava',
type: 'fire',
description: `Hopefully no usurpers have any "accidents".`,
cost: 4_200_000_000
}),
blueFire: heavenly({
name: 'Blue Fire',
type: 'fire',
description: `You can hear it singing with delight. 7x Fire CPS.`,
multiplier: 7,
cost: 25_200_000_000
}),
cuteFire: baby({
name: 'Cute Fire',
type: 'fire',
description: `I just met my perfect match...`,
cost: 150_000_000_000
}),
spoonerang: basic({
name: 'Spoonerang',
type: 'boomerang',
description: 'Scoops up HVAC mid-flight',
count: 1,
cost: 200_000_000
}),
boomerAng: basic({
name: 'Boomer-ang',
type: 'boomerang',
description: 'It\'s... old.',
count: 10,
cost: 1_200_000_000
}),
doubleRang: basic({
name: 'Double-rang',
type: 'boomerang',
description: 'You throw one, but somehow catch two',
count: 25,
cost: 10_000_000_000
}),
loyalRang: evil({
name: 'Loyal-rang',
type: 'boomerang',
description: `Frequently reports back to your throne on the state of your empire.`,
cost: 60_000_000_000
}),
youRang: heavenly({
name: 'You-rang',
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.',
multiplier: 7,
@ -250,29 +292,34 @@ module.exports = {
}),
lunarPower: basic({
name: 'Lunar Power',
type: 'moon',
description: 'Out with the sol, in with the lun!',
count: 1,
cost: 3_300_000_000
}),
womanOnTheMoon: basic({
name: 'Woman on the Moon',
type: 'moon',
description: 'There\'s no reason for it not to be a woman!',
count: 10,
cost: 39_700_000_000
}),
doubleCraters: basic({
name: 'Double-Craters',
type: 'moon',
description: 'Making every side look like the dark side.',
count: 25,
cost: 165_000_000_000
}),
tidalUpheaval: evil({
name: 'Tidal Upheaval',
type: 'moon',
description: `The hell with the ocean. That's valuable_ *the abstract concept of more power* _we're losing.`,
cost: 865_000_000_000
}),
newMoon: heavenly({
name: 'New Moon',
type: 'moon',
description: `Build a second moon to provide space for affordable housing. 6x Moon CPS.`,
multiplier: 6,
@ -280,29 +327,34 @@ module.exports = {
}),
glassButterfly: basic({
name: 'Glass Butterfly',
type: 'butterfly',
description: 'Not your grandma\'s universe manipulation.',
count: 1,
cost: 51_000_000_000
}),
monarchMigration: basic({
name: 'Monarch Migration',
type: 'butterfly',
description: 'This upgrade brought to you by milkweed.',
count: 10,
cost: 870_000_000_000
}),
quadWing: basic({
name: 'Quad-Wing',
type: 'butterfly',
description: 'Sounds a lot like a trillion bees buzzing inside your head.',
count: 25,
cost: 2_550_000_000_000
}),
venomousMoths: evil({
name: 'Venomous Moths',
type: 'butterfly',
description: 'Specifically manufactured for their horrifying brain-melt toxins.',
cost: 12_550_000_000_000
}),
quietingNectar: heavenly({
name: 'Quieting Nectar',
type: 'butterfly',
description: 'Calming and extra sweet. Soothes even human ails. 6x Butterfly CPS.',
multiplier: 6,
@ -310,29 +362,34 @@ module.exports = {
}),
silverMirror: basic({
name: 'Silver Mirror',
type: 'mirror',
description: 'Excellent for stabbing vampires.',
count: 1,
cost: 750_000_000_000
}),
pocketMirror: basic({
name: 'Pocket Mirror',
type: 'mirror',
description: 'Take your self-reflection on the go!',
count: 10,
cost: 18_000_000_000_000
}),
window: basic({
name: 'Window',
type: 'mirror',
description: 'Only through looking around you can you acquire the self reflection necessary to control the thermostat.',
count: 25,
cost: 37_500_000_000_000
}),
crackedMirror: evil({
name: 'Cracked Mirror',
type: 'mirror',
description: `YOU SMILE. DO NOT FEAR, THIS IS THE FACE OF A FRIEND.`,
cost: 222_000_000_000_000
}),
funHouseMirror: heavenly({
name: 'Fun-House Mirror',
type: 'mirror',
description: `yoU LOok so siLLY IN thesE THINgs. 5X mIRror CpS.`,
multiplier: 5,
@ -340,29 +397,34 @@ module.exports = {
}),
fzero: basic({
name: 'F-Zero',
type: 'quade',
description: 'Brings out his competitive spirit.',
count: 1,
cost: 10_000_000_000_000
}),
triHumpCamel: basic({
name: 'Trimedary Camel',
type: 'quade',
description: 'YEE HAW :trimedary_camel:',
count: 10,
cost: 200_000_000_000_000
}),
adam: basic({
name: 'Adam',
type: 'quade',
description: 'He could probably reach the thermostat if he wanted.',
count: 25,
cost: 500_000_000_000_000
}),
thatsNotQuade: evil({
name: `That's not Quade...`,
type: 'quade',
description: `The skinless face lacks even a moustache. Nevertheless, it pledges its allegiance.`,
cost: 3_000_000_000_000_000
}),
hannahMontanaLinux: heavenly({
name: 'Hannah Montana Linux',
type: 'quade',
description: `The patrician's choice. 4x Quade CPS.`,
multiplier: 4,
@ -370,29 +432,34 @@ module.exports = {
}),
latestNode: basic({
name: 'Latest Node',
type: 'hvacker',
description: 'The old one has terrible ergonomics, tsk tsk.',
count: 1,
cost: 140_000_000_000_000
}),
nativeFunctions: basic({
name: 'Native Functions',
type: 'hvacker',
description: 'Sometimes javascript just isn\'t fast enough.',
count: 10,
cost: 3_300_000_000_000_000
}),
gitCommits: basic({
name: 'Git Commits',
type: 'hvacker',
description: 'The heads of multiple people in a company are better than, for example, merely one head.',
count: 25,
cost: 7_000_000_000_000_000
}),
undefinedBehavior: evil({
name: 'Undefined Behavior',
type: 'hvacker',
description: `skREEEFDS☐☐☐☐☐it's☐jwtoo☐laate☐☐☐☐☐`,
cost: 42_000_000_000_000_000
}),
mutualUnderstanding: heavenly({
name: 'Mutual Understanding',
type: 'hvacker',
description: `lol fat chance, dummy. Points for trying, though. 3x Hvacker CPS`,
multiplier: 3,
@ -400,29 +467,34 @@ module.exports = {
}),
coffee: basic({
name: 'Coffee',
type: 'creator',
description: `Didn't you know? It makes you smarter. No consequencAAAAAA`,
count: 1,
cost: 1_960_000_000_000_000
}),
bribery: basic({
name: 'Bribery',
type: 'creator',
description: `How much could he be making that a couple bucks won't get me more HVAC?`,
count: 10,
cost: 32_300_000_000_000_000
}),
vim: basic({
name: 'Vim',
type: 'creator',
description: `*teleports behind you*`,
count: 25,
cost: 100_000_000_000_000_000
}),
regrets: evil({
name: 'Regrets',
type: 'creator',
description: `HE HAS NONE. HE LAUGHS.`,
cost: 600_000_000_000_000_000
}),
goVegan: heavenly({
name: 'Go Vegan',
type: 'creator',
description: `Unlock your vegan powers. 3x Creator CPS.`,
multiplier: 3,
@ -430,29 +502,34 @@ module.exports = {
}),
angelInvestors: basic({
name: 'Angel Investors',
type: 'smallBusiness',
description: 'Not so small NOW are we?',
count: 1,
cost: 3_140_000_000_000_000
}),
officeManager: basic({
name: 'Office Manager',
type: 'smallBusiness',
description: 'Sate your laborers with snacks.',
count: 10,
cost: 80_000_000_000_000_000
}),
undyingLoyalty: basic({
name: 'Undying Loyalty',
type: 'smallBusiness',
description: 'Your foolish employees bow to your every whim, regardless of salary.',
count: 25,
cost: 138_000_000_000_000_000
}),
deathSquad: evil({
name: 'Death Squad',
type: 'smallBusiness',
description: `pwease don't unionize uwu :pleading_face:`,
cost: 858_000_000_000_000_000
}),
coop: heavenly({
name: 'Co-Op',
type: 'smallBusiness',
description: `By the people, for the people. 2x smallBusiness CPS`,
multiplier: 2,
@ -460,36 +537,77 @@ module.exports = {
}),
corporateBuyouts: basic({
name: 'Corporate Buyouts',
type: 'bigBusiness',
description: 'The cornerstone of any family-run business.',
count: 1,
cost: 28_140_000_000_000_000
}),
politicalSway: basic({
name: 'Political Sway',
type: 'bigBusiness',
description: `What's a bit of lobbying between friends?`,
count: 10,
cost: 560_000_000_000_000_000
}),
humanDiscontent: basic({
name: 'Human Discontent',
type: 'bigBusiness',
description: 'A sad populace is a spendy populace!',
count: 25,
cost: 1_372_000_000_000_000_000
}),
weJustKillPeopleNow: evil({
name: 'We Just Kill People Now',
type: 'bigBusiness',
description: 'It is extremely difficult to get more evil than we already were. Nevertheless,',
cost: 7_072_000_000_000_000_000
}),
makePublic: heavenly({
name: 'Make Public',
type: 'bigBusiness',
description: `Downplay immediate profit for more long-term benefits. 2x bigBusiness CPS.`,
multiplier: 2,
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: {
name: 'Homage',
type: 'general',
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,
@ -498,11 +616,51 @@ module.exports = {
effect: (itemCps, user) => itemCps * 1.1
},
iLoveHvac: {
name: 'iLoveHvac',
type: 'general',
description: 'The power of love increases your overall CPS by 10%',
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
emoji: 'heart',
cost: 100_000_000_000_000,
effect: (itemCps, user) => 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 achievements = require('./achievements')
const buyableItems = require('./buyableItems')
const upgrades = require('./upgrades')
const { quackStore, getChaos } = require('./quackstore')
let upgrades
const setUpgrades = upg => {
upgrades = upg
}
const saveFile = 'hvacoins.json'
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
}
const parseOr = (parseable, orFunc) => {
const parseOr = (parseable, fallback) => {
try {
if (typeof parseable === 'function') {
parseable = parseable()
}
return JSON.parse(parseable)
} catch (e) {
logError(e)
return orFunc()
return fallback()
}
}
@ -53,13 +60,18 @@ const makeBackup = () => {
}
let saves = 0
const saveGame = (force = true) => {
if (saves % 100 === 0) {
const saveGame = (after, force = true) => {
if (saves % 20 === 0) {
makeBackup()
}
saves += 1
if (force || saves % 10 === 0) {
console.log('SAVING GAME')
if (after) {
console.log(`SAVING GAME after ${after}`)
} else {
console.log('SAVING GAME')
}
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],
['nonillion', 1_000_000_000_000_000_000_000_000_000_000],
['octillion', 1_000_000_000_000_000_000_000_000_000],
['septtillion', 1_000_000_000_000_000_000_000_000],
['septillion', 1_000_000_000_000_000_000_000_000],
['sextillion', 1_000_000_000_000_000_000_000],
['quintillion', 1_000_000_000_000_000_000],
['quadrillion', 1_000_000_000_000_000],
@ -101,18 +113,24 @@ const bigNumberWords = [
['million', 1_000_000],
]
const commas = (num, precise = false) => {
const commas = (num, precise = false, skipWords = false) => {
num = Math.round(num)
if (num === 1) {
return 'one'
}
const bigNum = bigNumberWords.find(([, base]) => num >= base)
if (bigNum && !precise) {
const [name, base] = bigNum
const nummed = (num / base).toPrecision(3)
if (skipWords) {
return nummed
}
return `${nummed} ${name}`
}
return num.toLocaleString()
}
const parseAll = (str, allNum) => {
const parseAll = (str, allNum, user) => {
if (!str) {
return NaN
}
@ -144,11 +162,21 @@ const parseAll = (str, allNum) => {
case 'one hunna':
return 100
}
if (user && buyableItems[str]) {
return calculateCost({ itemName: str, user, quantity: 1 })
}
console.log('STR', str)
if (str.match(/^\d+$/)) {
return parseInt(str)
}
if (allNum && str.match(/^\d+%$/)) {
const percent = parseFloat(str) / 100
if (percent > 1 || percent < 0) {
return NaN
}
return Math.round(percent * allNum)
}
if (str.match(/^\d+\.\d+$/)) {
return Math.round(parseFloat(str))
@ -162,6 +190,16 @@ const parseAll = (str, allNum) => {
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 { users, nfts, squad } = game
@ -182,7 +220,7 @@ const addAchievement = (user, achievementName, say) => {
}
setTimeout(async () => {
user.achievements[achievementName] = true
saveGame()
saveGame(`${user.name} earned ${achievementName}`)
await say(`You earned the achievement ${achievements[achievementName].name}!`)
}, 500)
}
@ -199,22 +237,17 @@ const getIdFromName = name => {
return null;
}
const getUser = userId => {
if (!users[userId]) {
users[userId] = {
coins: 0,
items: {},
upgrades: {},
achievements: {},
coinsAllTime: 0,
prestige: 0
}
} else {
users[userId].items ??= {}
users[userId].upgrades ??= {}
users[userId].achievements ??= {}
users[userId].coinsAllTime ??= users[userId].coins
users[userId].prestige ??= 0
const getUser = (userId, updateCoins = false) => {
users[userId] ??= {}
users[userId].coins ??= 0
users[userId].items ??= {}
users[userId].upgrades ??= {}
users[userId].achievements ??= {}
users[userId].coinsAllTime ??= users[userId].coins
users[userId].prestige ??= 0
users[userId].startDate ??= new Date()
if (updateCoins) {
users[userId].coins = getCoins(userId)
}
return users[userId]
}
@ -251,6 +284,7 @@ const squadUpgrades = {
tastyKeyboards: {
name: 'Tasty Keyboards',
description: 'Delicious and sticky. Boosts CPS by 20% for everyone.',
effect: cps => cps * 1.2,
cost: 10_000_000_000_000,
emoji: 'keyboard'
@ -298,6 +332,11 @@ const quackGradeMultiplier = user => {
return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1)
}
const petQuackGradeMultiplier = user => {
const userQuackgrades = user.quackUpgrades?.pet || []
return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), petBoost())
}
const singleItemCps = (user, itemName) => {
const baseCps = buyableItems[itemName].earning
// console.log('')
@ -326,6 +365,9 @@ const singleItemCps = (user, itemName) => {
const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1)
// console.log('squadGradeMultiplier', squadGradeMultiplier)
const petMultiplier = petQuackGradeMultiplier(user)
//console.log('petMultiplier', petMultiplier)
const total =
baseCps *
achievementMultiplier *
@ -333,7 +375,8 @@ const singleItemCps = (user, itemName) => {
generalUpgradeCps *
quackGrade *
pMult *
squadGradeMultiplier
squadGradeMultiplier *
petMultiplier
// console.log('Single Item CPS:', total)
@ -372,6 +415,7 @@ const definitelyShuffle = (str, percentOdds) => {
let shuffled = str
while (shuffled === str) {
shuffled = shufflePercent(str, percentOdds)
console.log('Shuffling... "' + shuffled + '"')
}
return shuffled
}
@ -436,6 +480,71 @@ game.stonkMarket ??= {
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 = {
saveGame,
makeBackup,
@ -468,5 +577,10 @@ module.exports = {
userHasCheckedQuackgrade,
fuzzyMatcher,
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 haunted = false
//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) {
await say(`Command '${words[0]}' not found`)
return

View File

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

View File

@ -1,19 +1,22 @@
const { App: SlackApp } = require('@slack/bolt')
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 dailyStandupChannelId = 'C03L533AU3Z'
const pollingMinutes = 5
const pollingPeriod = 1000 * 60 * pollingMinutes
const MAX_POLLS = 3
const HOURS_PER_WINDOW = 2
const colderEmoji = 'snowflake'
const hotterEmoji = 'fire'
const goodEmoji = '+1'
let app
try {
app = new SlackApp({
const app = new SlackApp({
token: config.slackBotToken,
signingSecret: config.slackSigningSecret,
appToken: config.slackAppToken,
@ -23,9 +26,6 @@ try {
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
// console.log('techThermostatChannelId', temperatureChannelId)
// })
} catch (e) {
console.log('Failed to initialize SlackApp', e)
}
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater']
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
@ -41,7 +41,7 @@ const sendHelp = async (say, prefix) => {
text: prefix +
`Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` +
'\'Hotter\' and \'Colder\' votes offset. E.g. with votes Hotter - 4, Colder - 3, and Content - 2, the temp won\'t change.\n' +
'At this time I am not capable of actually changing the temperature. Go bug Quade.'
'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 = {
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 users = parseOr(fs.readFileSync('./users.json', 'utf-8'),
() => ({}))
const buildSayPrepend = ({ say, prepend }) => async msg => {
if (typeof(msg) === 'string') {
@ -99,10 +73,11 @@ const buildSayPrepend = ({ say, prepend }) => async msg => {
}
process.once('SIGINT', code => {
saveGame(true)
saveGame(null, true)
process.exit()
})
let pollHistory = []
const activePolls = {}
const testId = 'U028BMEBWBV_TEST'
let testMode = false
@ -113,18 +88,18 @@ app.event('message', async ({ event, context, client, say }) => {
userName: users[event.user]
})
}
if (event?.user === users.Sage) {
if (event?.user === users.Admin) {
if (event?.text.startsWith('!')) {
if (testMode) {
await messageSage('Currently in test mode!')
await messageAdmin('Currently in test mode!')
}
}
if (event?.text === '!test') {
testMode = !testMode
await messageSage(`TestMode: ${testMode} with ID ${testId}`)
await messageAdmin(`TestMode: ${testMode} with ID ${testId}`)
} else if (event?.text === '!notest') {
testMode = false
await messageSage(`TestMode: ${testMode}`)
await messageAdmin(`TestMode: ${testMode}`)
}
if (testMode) {
event.user = testId
@ -136,16 +111,21 @@ app.event('message', async ({ event, context, client, say }) => {
if (event.user) {
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') {
saveGame(true)
saveGame(null, true)
process.exit(1)
} else if (event.text === '!!restart') {
saveGame(true)
process.exit()
if (Object.entries(activePolls).length === 0) {
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')) {
await postToTechThermostatChannel(event.text.substring(4).trim())
await postToTechThermostatChannel(event.text.substring(4).trim().replace('@here', '<!here>'))
return
}
}
@ -156,17 +136,37 @@ app.event('message', async ({ event, context, client, say }) => {
return
}
if (!pollTriggers.includes(eventText)) {
if (!pollTriggers.includes(eventText) || event.user === users.John) {
if (halfTriggers.includes(eventText)) {
await sendHelp(say, 'It looks like you might want to change the temperature.')
}
return
}
if (event.channel !== temperatureChannelId) {
return say(`Please request polls in the appropriate channel.`)
}
if (activePolls[event.channel]) {
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
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
const pollTs = await startPoll()
@ -215,11 +215,11 @@ app.event('message', async ({ event, context, client, say }) => {
let text
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.'
requestTempChange('Hotter')
} 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.'
requestTempChange('Colder')
} else {
@ -230,11 +230,18 @@ app.event('message', async ({ event, context, client, say }) => {
await postToTechThermostatChannel({ text })
delete activePolls[event.channel]
if (pendingRestart && Object.entries(activePolls).length === 0) {
await messageAdmin('Performing pending restart!')
saveGame(null, true)
process.exit(0)
}
}, pollingPeriod)
})
let pendingRestart = false
;(async () => {
await app.start().catch(console.error)
await app.start()
console.log('Slack Bolt has started')
})()
@ -247,7 +254,7 @@ const postToTechThermostatChannel = async optionsOrText => {
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) => {
if (optionsOrText === null || typeof optionsOrText !== 'object') {
@ -262,7 +269,7 @@ const startPoll = async () => {
const sent = await postToTechThermostatChannel({
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.` +
'\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({
app,
@ -281,25 +288,44 @@ const requestTempChange = change => {
tempChangeListeners.forEach(listener => listener(change))
}
// noinspection HttpUrlsUsage
const encodeData = (key, data) =>
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
const decodeData = (key, message) => {
const regex = new RegExp(`http://${key}ZZZ[^|]*`)
let match = message.match(regex)
if (!match) {
return match
try {
const regex = new RegExp(`http://${key}ZZZ[^|]*`)
let match = message.match(regex)
if (!match) {
return match
}
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
} catch (e) {
console.error(e)
return null
}
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
}
const onReaction = listener => reactionListeners.push(listener)
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 }) => {
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 {
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
} catch (e) {
@ -307,9 +333,12 @@ onReaction(async ({ event }) => {
}
})
setSlackAppClientChatUpdate(app.client.chat.update)
module.exports = {
app,
temperatureChannelId,
dailyStandupChannelId,
onAction: app.action,
getMessage,
updateMessage: app.client.chat.update,
@ -319,10 +348,12 @@ module.exports = {
onReaction,
encodeData,
decodeData,
messageSage,
messageAdmin,
messageIn,
testMode,
testId,
users,
buildSayPrepend
buildSayPrepend,
pollTriggers,
pendingRestart
}