Many additions:

Add several new achievements
Add price querying with ?b
Simplify commands' argument-handling
Some spooky stuff
New quackgrades
Stormy weather
Rebalanced some rare-event odds
!u buttons
Reworked prestige emojis
Save on every command
Add stonks
More temp poll triggers
Redemption upgrades more powerful
Fuzzy matching for usernames and buyables
This commit is contained in:
Sage Vaillancourt 2022-05-19 11:09:16 -04:00
parent 6dabe9d85a
commit ad021cf9a5
14 changed files with 1152 additions and 332 deletions

View File

@ -11,4 +11,4 @@ fetch(url, {
headers: headers
// credentials: 'user:passwd'
}).then(response => response.json())
.then(json => console.log(json))
.then(json => console.log('json', json))

View File

@ -34,6 +34,16 @@ module.exports = {
description: 'I like big bets, and that\'s the truth',
emoji: 'slot_machine'
},
hugeBets: {
name: 'Make a bet over 100T',
description: `That's so bonk`,
emoji: 'game_die'
},
mondoBets: {
name: 'Make a bet over 100 Quadrillion',
description: 'H I G H R O L L E R',
emoji: '8ball'
},
ignited: {
name: 'You light my fire, baby',
description: 'And you pay attention to descriptions!',
@ -45,6 +55,72 @@ module.exports = {
description: 'I\'m beginning to feel like a rat god, rat god.',
emoji: 'mouse2'
},
mathematician: {
name: 'Own 100 Accountants',
description: 'They rejoice at the appearance of a third digit.',
emoji: 'male-office-worker'
},
iPod: {
name: 'Own 100 Whales',
description: `With the new iPod, you can hold 100's of songs.`,
emoji: 'whale'
},
fire100: {
name: 'Own 100 Fires',
description: `Wow, that's bright.`,
emoji: 'fire'
},
train100: {
name: 'Own 100 Trains',
description: `That's every train in America you've got there.`,
emoji: 'train2'
},
boom100: {
name: 'Own 100 Boomerangs',
description: `LOUD WOOSHING`,
emoji: 'boomerang'
},
moon100: {
name: 'Own 100 Moons',
description: `Space Cadet`,
emoji: 'new_moon_with_face'
},
mirror100: {
name: 'Own 100 Mirrors',
description: `Disco Ball`,
emoji: 'mirror'
},
butterfly100: {
name: 'Own 100 Butterflies',
description: `Delicate yet powerful.`,
emoji: 'butterfly'
},
quade100: {
name: 'Own 100 Quades',
description: `Your Ops are super Devved right now.`,
emoji: 'quade'
},
hvacker100: {
name: 'Own 100 Hvackers',
description: `Did Sage finally make his git repo public?`,
emoji: 'hvacker_angery'
},
creator100: {
name: 'Own 100 Creators',
description: `_Stern look_`,
emoji: 'question'
},
smallBusiness100: {
name: 'Own 100 Small Businesses',
description: `Enough to run a small city.`,
emoji: 'convenience_store'
},
bigBusiness100: {
name: 'Own 100 Big Businesses',
description: `I mean... that's basically all of them.`,
emoji: 'office'
},
weAllNeedHelp: {
name: 'View the \'!coin\' help',
description: 'We all need a little help sometimes',
@ -85,5 +161,11 @@ module.exports = {
name: 'Take a peek at the lore',
description: 'It\'t gotta be worth your time somehow.',
emoji: 'books'
},
theOtherSide: {
name: 'Die and be reborn',
description: 'You have seen the other side, and do not fear it.',
emoji: 'white_square'
}
}

View File

@ -1,5 +1,5 @@
const buyableItems = require('./buyableItems')
const { commas, saveGame, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter } = require('./utils')
const { commas, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter, fuzzyMatcher } = require('./utils')
const slack = require('../../slack')
const calculateCost = ({ itemName, user, quantity = 1 }) => {
@ -17,9 +17,9 @@ const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) =>
const itemCps = Math.round(singleItemCps(user, itemName))
return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(itemCps)} CPS\n_${description}_`
}
const canView = highestCoins => ([, item]) => item.baseCost < (highestCoins || 1) * 101
const canView = (item, highestCoins) => item.baseCost < (highestCoins || 1) * 101
const buyableText = (highestCoins, user) => Object.entries(buyableItems)
.filter(canView(highestCoins))
.filter(([, item]) => canView(item, highestCoins))
.map(getItemHeader(user))
.join('\n\n') +
'\n\n:grey_question::grey_question::grey_question:' +
@ -73,8 +73,8 @@ const buyText2 = (highestCoins, user) => {
return ({
text: buyableText(highestCoins, user),
blocks: Object.entries(buyableItems)
.filter(canView(highestCoins))
.map(([itemName, item]) => {
.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 })
@ -90,58 +90,65 @@ const maxQuantity = ({ itemName, user, currentCoins }) => {
return quantity
}
const buyRoute = async ({ event, say, words, user }) => {
const buying = words[1]
const buyRoute = async ({ event, say, args, user }) => {
const buying = args[0]
setHighestCoins(event.user)
const query = event?.text?.startsWith('?b ') || event?.text?.startsWith('?buy ')
if (!buying) {
const highestCoins = user.highestEver || user.coins || 1
if (buyableItems.quade.baseCost < highestCoins * 100) {
if (canView(buyableItems.quade, highestCoins)) {
addAchievement(user, 'seeTheQuade', say)
}
await say(buyText2(highestCoins, user))
return
}
const buyable = buyableItems[buying]
const matcher = fuzzyMatcher(buying)
const buyable = Object.entries(buyableItems).find(([name]) => matcher.test(name))
if (!buyable) {
await say('That item does not exist!')
return
}
const [buyableName, buyableItem] = buyable
let quantity
const currentCoins = user.coins
const max = maxQuantity({ itemName: buying, user, currentCoins })
if (words[2] === 'max') {
const max = maxQuantity({ itemName: buyableName, user, currentCoins })
if (!args[1]) {
quantity = 1
} else if (args[1] === 'max') {
quantity = max
} else {
quantity = Math.round(chaosFilter(parseInt(words[2] || '1'), 0.2, user, max) || 1)
if (query) {
quantity = parseInt(args[1])
} else {
quantity = Math.round(chaosFilter(parseInt(args[1]), 0.2, user, max) || 1)
}
}
if (!quantity || quantity < 1) {
await say('Quantity must be a positive integer')
return
}
const realCost = calculateCost({ itemName: buying, user, quantity })
const realCost = calculateCost({ itemName: buyableName, user, quantity })
if (query) {
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)}`)
return
}
user.coins -= realCost
user.items[buying] = user.items[buying] || 0
user.items[buying] += quantity
user.items[buyableName] = user.items[buyableName] || 0
user.items[buyableName] += quantity
if (buying === 'mouse' && user.items.mouse >= 100) {
addAchievement(user, 'ratGod', say)
if (user.items[buyableName] >= 100) {
addAchievement(user, buyableItems[buyableName].own100Achievement, say)
}
if (quantity === 1) {
await say(`You bought one :${buyable.emoji}:`)
} else {
await say(`You bought ${quantity} :${buyable.emoji}:`)
}
saveGame()
const countString = quantity === 1 ? 'one' : quantity
await say(`You bought ${countString} :${buyableItem.emoji}:`)
}
const buyButton = async ({ body, ack, say, payload }) => {
@ -153,7 +160,8 @@ const buyButton = async ({ body, ack, say, payload }) => {
}
const user = getUser(event.user)
const words = ['', buying, body.actions[0].text]
await buyRoute({ event, say, words, user })
const [commandName, ...args] = words
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,

View File

@ -3,84 +3,98 @@ module.exports = {
baseCost: 100,
earning: 1,
emoji: 'mouse2',
description: 'A mouse to steal coins for you.'
description: 'A mouse to steal coins for you.',
own100Achievement: 'ratGod',
},
accountant: {
baseCost: 1_100,
earning: 8,
emoji: 'male-office-worker',
description: 'Legally make money from nothing!'
description: 'Legally make money from nothing!',
own100Achievement: 'mathematician',
},
whale: {
baseCost: 12_000,
earning: 47,
emoji: 'whale',
description: 'Someone to spend money on your HVAC Coin mining app.'
description: 'Someone to spend money on your HVAC Coin mining app.',
own100Achievement: 'iPod',
},
train: {
baseCost: 130_000,
earning: 260,
emoji: 'train2',
description: 'Efficiently ship your most valuable coins.'
description: 'Efficiently ship your most valuable coins.',
own100Achievement: 'fire100',
},
fire: {
baseCost: 1_400_000,
earning: 1_400,
emoji: 'fire',
description: 'Return to the roots of HVAC.'
description: 'Return to the roots of HVAC.',
own100Achievement: 'train100',
},
boomerang: {
baseCost: 20_000_000,
earning: 7_800,
emoji: 'boomerang',
description: 'Your coin always seems to come back.'
description: 'Your coin always seems to come back.',
own100Achievement: 'boom100',
},
moon: {
baseCost: 330_000_000,
earning: 44_000,
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',
},
butterfly: {
baseCost: 5_100_000_000,
earning: 260_000,
emoji: 'butterfly',
description: 'Create the exact worldly chaos to bit-flip HVAC Coins into existence on your computer.'
description: 'Create the exact worldly chaos to bit-flip HVAC Coins into existence on your computer.',
own100Achievement: 'butterfly100',
},
mirror: {
baseCost: 75_000_000_000,
earning: 1_600_000,
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',
},
quade: {
baseCost: 1_000_000_000_000,
earning: 10_000_000,
emoji: 'quade',
description: 'Has thumbs capable of physically manipulating the thermostat.'
description: 'Has thumbs capable of physically manipulating the thermostat.',
own100Achievement: 'hvacker100',
},
hvacker: {
baseCost: 14_000_000_000_000,
earning: 65_000_000,
emoji: 'hvacker_angery',
description: 'Harness the power of the mad god himself.'
description: 'Harness the power of the mad god himself.',
own100Achievement: 'creator100',
},
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.'
description: 'The elusive creator of Hvacker takes a favorable look at your CPS.',
own100Achievement: 'smallBusiness100',
},
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.'
description: 'The place where the creator of Hvacker goes to work.',
own100Achievement: 'bigBusiness100',
},
bigBusiness: {
baseCost: 26_210_000_000_000_000,
earning: 23_650_000_000,
emoji: 'office',
description: 'The place where the smallBusiness goes to work.'
description: 'The place where the smallBusiness goes to work.',
own100Achievement: 'ratGod',
}
}

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,8 @@ const lore = [
l(`And the ninth...`),
l(`Well, the ninth might actually amount to something.`),
l(`https://i.imgur.com/eFreg7Y.gif\n`),
//l(`As you might imagine, the ninth egg was I, the almighty Hvacker.`)
]
slack.onReaction(async ({ event, say }) => {
@ -56,11 +58,11 @@ slack.onReaction(async ({ event, say }) => {
}
return
}
console.log(lore[user.lore])
console.log('lore:', lore[user.lore])
await say(lore[user.lore].correctResponse)
user.lore += 1
saveGame()
} catch (e) {console.error(e)}
} catch (e) {console.error('onReaction error', e)}
})
const encodeLore = loreNumber => lore[loreNumber].text.startsWith(':') && lore[loreNumber].text.endsWith(':') ? '' :
@ -77,35 +79,35 @@ const loreMessage = (user, say) => {
return `Sorry. I'd love to tell you more, but I'm tired. Please check back later.`
}
const loreRoute = async ({ say, words, user, isAdmin }) => {
const loreRoute = async ({ say, args, user, isAdmin }) => {
user.lore ??= 0
if (!words[1]) {
if (!args[0]) {
const message = loreMessage(user, say)
await say(message)
if (!lore[user.lore]?.correctReactions) {
user.lore += 1
}
saveGame()
//saveGame()
console.log('Sent ' + user.name + ':\n' + message)
return
}
if (words[1] === 'reset') {
if (args[0] === 'reset') {
user.lore = 0
saveGame()
//saveGame()
return say(`I have reset your place in the story.`)
}
if (isAdmin) {
if (words[1] === 'all') {
if (args[0] === 'all') {
let loreMessage = ''
for (let i = 0; i < user.lore; i++) {
loreMessage += lore[i].text + (lore[i].correctResponse || '') + '\n'
}
return say(loreMessage)
}
const jumpTo = parseInt(words[1])
const jumpTo = parseInt(args[0])
if (!isNaN(jumpTo)) {
user.lore = jumpTo
saveGame()
//saveGame()
}
}
}

View File

@ -1,4 +1,4 @@
const { commas, saveGame, quackGradeMultiplier, prestigeMultiplier, makeBackup } = require('./utils')
const { commas, quackGradeMultiplier, prestigeMultiplier, makeBackup, userHasCheckedQuackgrade } = require('./utils')
const { quackStore } = require('./quackstore')
const possiblePrestige = coins => {
@ -17,10 +17,10 @@ const totalCostForPrestige = prestigeLevel => {
return (tpcRecMemo[prestigeLevel]) || (tpcRecMemo[prestigeLevel] = 1_000_000_000_000 * Math.pow(prestigeLevel, 3) + totalCostForPrestige(prestigeLevel - 1))
}
const prestigeRoute = async ({ say, words, user }) => {
const prestigeRoute = async ({ say, args, user }) => {
const possible = possiblePrestige(user.coinsAllTime)
const current = user.prestige ??= 0
if (words[1] === 'me') {
if (args[0] === 'me') {
await say(
'This will permanently remove all of your items, upgrades, and coins!\n\n' +
'Say \'!!prestige me\' to confirm.'
@ -53,18 +53,18 @@ const prestigeConfirmRoute = async ({ event, say, user }) => {
user.quacks += (possible - user.prestige)
user.prestige = possible
user.highestEver = 0
user.coins = 0
user.items = {}
user.items = {};
const starterUpgrades = (user.quackUpgrades?.starter || [])
starterUpgrades.forEach(upgradeName => quackStore[upgradeName].effect(user))
user.upgrades = {}
saveGame()
await say('You prestiged! Check out !quackstore to see what you can buy!')
}
const quackStoreListing = (showCost = true) => ([name, upgrade]) =>
showCost
? `:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
: `:${upgrade.emoji}: *${name}* - Worth *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
`:${upgrade.emoji}: *${name}* - ${showCost ? 'Costs' : 'Worth'} *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
const allUserQuackUpgrades = user =>
Object.entries(user.quackUpgrades || {})
@ -75,6 +75,7 @@ const hasPreReqs = user => ([name, upgrade]) => {
return true
}
const allUserUpgrades = allUserQuackUpgrades(user)
console.log('allUserUpgrades', allUserUpgrades)
return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq))
}
@ -92,27 +93,31 @@ 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, words }) => {
const quackStoreRoute = async ({ user, say, args }) => {
user.quackUpgrades ??= {}
const quacks = user.quacks ??= 0
if (!words[1]) {
if (!args[0]) {
await say(quackStoreText(user))
return
}
console.log(`Trying to buy ${words[1]}`)
const quackItem = quackStore[words[1]]
if (!quackItem || !unownedQuackItems(user).find(([name]) => name === words[1])) {
await say(`'${words[1]}' is not available in the quack store!`)
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
}
if (quackItem.cost > quacks) {
await say(`${words[1]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
await say(`${args[0]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
return
}
user.quacks -= quackItem.cost
user.quackUpgrades[quackItem.type] ??= []
user.quackUpgrades[quackItem.type].push(words[1])
saveGame()
user.quackUpgrades[quackItem.type].push(args[0])
if (quackItem.type === 'starter') {
quackItem.effect(user)
}
await say(`You bought ${args[0]}!`)
//saveGame()
}
const ownedQuacksText = user =>

View File

@ -31,9 +31,6 @@ const quackStore = {
//+ '_\n_Averages a 26% CPS boost.',
preReqs: ['nuclearFuel'],
effect: (cps, user) => {
if (user.name !== 'Sage') {
console.log('Chaos Multiplier', getChaos(Math.round(user.interactions / 50)))
}
return cps * getChaos(Math.round(user.interactions / 50))
},
cost: 10
@ -44,10 +41,67 @@ const quackStore = {
type: 'lightning',
emoji: 'rose',
description: 'Smells nice. Makes lightning twice as likely to strike.',
//+ '_\n_Averages a 26% CPS boost.',
effect: lightningOdds => lightningOdds * 2,
preReqs: ['nuclearFuel'],
cost: 10
},
// Checked Upgrades. Have no effect(), but their existence is referred to elsewhere.
theGift: {
name: 'The Gift',
type: 'checked',
emoji: 'eye-in-speech-bubble',
description: 'Become forewarned of certain events...',
preReqs: ['dryerSheet', 'chaos'],
cost: 10
},
theVoice: {
name: 'The Voice',
type: 'checked',
emoji: 'loud_sound',
description: 'Unlocks the !speak command',
preReqs: ['dryerSheet', 'chaos'],
cost: 50
},
cheeseBaby: {
name: 'cheeseBaby',
type: 'starter',
emoji: 'baby_symbol',
description: 'Start each prestige with 5 mice',
preReqs: ['dryerSheet', 'chaos'],
effect: user => {
user.items.mouse ??= 0
user.items.mouse += 5
},
cost: 5
},
silverSpoon: {
name: 'Silver Spoon',
type: 'starter',
emoji: 'spoon',
description: 'Start each prestige with 5 accountants',
preReqs: ['cheeseBaby'],
effect: user => {
user.items.accountant ??= 0
user.items.accountant += 5
},
cost: 10
},
oceanMan: {
name: 'Ocean Man',
type: 'starter',
emoji: 'ocean',
description: 'Start each prestige with 5 whales',
preReqs: ['silverSpoon'],
effect: user => {
user.items.whale ??= 0
user.items.whale += 5
},
cost: 20
}
}

View File

@ -1,3 +1,4 @@
module.exports = {
horrorEnabled: false
horrorEnabled: false,
admins: ['Sage']
}

View File

@ -14,20 +14,22 @@ const evil = ({ type, description, cost }) => basic({
extraCondition: (user, squadGrades) => squadGrades?.includes('discardHumanMorals'),
})
const heavenly = ({ type, description, cost }) => basic({
const heavenly = ({ type, description, cost, multiplier = 2 }) => ({
type,
description,
count: 60,
condition: (user, squadGrades) => user.items[type] >= 60 && squadGrades?.includes('redemption'),
cost,
extraCondition: (user, squadGrades) => squadGrades?.includes('redemption'),
effect: cps => cps * multiplier
})
const disabled = () => false
const baby = ({ type, description, cost }) => basic({
type,
description,
count: 80,
count: 70,
cost,
extraCondition: (user, squadGrades) => squadGrades?.includes('redemption'),
extraCondition: disabled
})
const geometry = ({ type, description, cost }) => basic({
@ -35,7 +37,7 @@ const geometry = ({ type, description, cost }) => basic({
description,
count: 100,
cost,
extraCondition: (user, squadGrades) => squadGrades?.includes('redemption'),
extraCondition: disabled
})
const universitality = ({ type, description, cost }) => basic({
@ -43,7 +45,7 @@ const universitality = ({ type, description, cost }) => basic({
description,
count: 100,
cost,
extraCondition: (user, squadGrades) => squadGrades?.includes('redemption'),
extraCondition: disabled
})
module.exports = {
@ -72,8 +74,14 @@ module.exports = {
}),
hoodedMice: heavenly({
type: 'mouse',
description: 'These monks have nearly reached enlightenment.',
description: 'These monks have nearly reached enlightenment. 10x Mouse CPS.',
cost: 1_000_000,
multiplier: 10,
}),
babyMouse: baby({
type: 'mouse',
description: 'Squeak!',
cost: 6_000_000,
}),
fasterComputers: basic({
@ -101,8 +109,14 @@ module.exports = {
}),
charityFund: heavenly({
type: 'accountant',
description: 'THIS one is more than just a tax break.',
description: 'THIS one is more than just a tax break. 9x Accountant CPS.',
cost: 16_333_333,
multiplier: 9,
}),
mathBaby: baby({
type: 'accountant',
description: '2 + 2 = WAAH!',
cost: 99_999_999,
}),
biggerBlowhole: basic({
@ -130,8 +144,14 @@ module.exports = {
}),
whaleChoir: heavenly({
type: 'whale',
description: `Their cleansing songs reverberate through the sea.`,
cost: 144_000_000
description: `Their cleansing songs reverberate through the sea. 8x Whale CPS.`,
cost: 144_000_000,
multiplier: 8,
}),
smolWhales: baby({
type: 'whale',
description: ``,
cost: 8_400_000_000
}),
greasyTracks: basic({
@ -159,7 +179,8 @@ module.exports = {
}),
toyTrain: heavenly({
type: 'train',
description: 'Something simple. Toot toot!',
description: 'Toot toot! 8x Train CPS.',
multiplier: 8,
cost: 2_220_000_000
}),
@ -171,7 +192,7 @@ module.exports = {
}),
extremelyDryFuel: basic({
type: 'fire',
description: 'Use the ignite command for a secret achievement.',
description: 'Hey, psst, hey. Use the ignite command for a secret achievement.',
count: 10,
cost: 163_000_000
}),
@ -188,9 +209,15 @@ module.exports = {
}),
blueFire: heavenly({
type: 'fire',
description: `You can hear it singing with delight.`,
description: `You can hear it singing with delight. 7x Fire CPS.`,
multiplier: 7,
cost: 25_200_000_000
}),
cuteFire: baby({
type: 'fire',
description: `I just met my perfect match...`,
cost: 150_000_000_000
}),
spoonerang: basic({
type: 'boomerang',
@ -217,7 +244,8 @@ module.exports = {
}),
youRang: heavenly({
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.',
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,
cost: 360_000_000_000
}),
@ -246,7 +274,8 @@ module.exports = {
}),
newMoon: heavenly({
type: 'moon',
description: `Build a second moon to provide space for affordable housing.`,
description: `Build a second moon to provide space for affordable housing. 6x Moon CPS.`,
multiplier: 6,
cost: 5_190_000_000_000
}),
@ -275,7 +304,8 @@ module.exports = {
}),
quietingNectar: heavenly({
type: 'butterfly',
description: 'Calming and extra sweet. Soothes even human ails.',
description: 'Calming and extra sweet. Soothes even human ails. 6x Butterfly CPS.',
multiplier: 6,
cost: 75_300_000_000_000
}),
@ -304,7 +334,8 @@ module.exports = {
}),
funHouseMirror: heavenly({
type: 'mirror',
description: `yoU LOok so siLLY IN thesE THINgs`,
description: `yoU LOok so siLLY IN thesE THINgs. 5X mIRror CpS.`,
multiplier: 5,
cost: 1_330_000_000_000_000
}),
@ -333,7 +364,8 @@ module.exports = {
}),
hannahMontanaLinux: heavenly({
type: 'quade',
description: `The patrician's choice.`,
description: `The patrician's choice. 4x Quade CPS.`,
multiplier: 4,
cost: 18_000_000_000_000_000
}),
@ -362,7 +394,8 @@ module.exports = {
}),
mutualUnderstanding: heavenly({
type: 'hvacker',
description: `lol fat chance, dummy. Points for trying, though`,
description: `lol fat chance, dummy. Points for trying, though. 3x Hvacker CPS`,
multiplier: 3,
cost: 250_000_000_000_000_000
}),
@ -391,7 +424,8 @@ module.exports = {
}),
goVegan: heavenly({
type: 'creator',
description: `Unlock your vegan powers.`,
description: `Unlock your vegan powers. 3x Creator CPS.`,
multiplier: 3,
cost: 3_600_000_000_000_000_000
}),
@ -420,7 +454,8 @@ module.exports = {
}),
coop: heavenly({
type: 'smallBusiness',
description: `By the people, for the people.`,
description: `By the people, for the people. 2x smallBusiness CPS`,
multiplier: 2,
cost: 5_140_000_000_000_000_000
}),
@ -449,7 +484,8 @@ module.exports = {
}),
makePublic: heavenly({
type: 'bigBusiness',
description: `Downplay immediate profit for more long-term benefits.`,
description: `Downplay immediate profit for more long-term benefits. 2x bigBusiness CPS.`,
multiplier: 2,
cost: 42_000_000_000_000_000_000
}),

View File

@ -7,7 +7,7 @@ const { quackStore, getChaos } = require('./quackstore')
const saveFile = 'hvacoins.json'
const logError = msg => msg ? console.error(msg) : () => { /* Don't log empty message */ }
const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ }
const loadGame = () => {
const game = parseOr(fs.readFileSync('./' + saveFile, 'utf-8'),
@ -53,12 +53,15 @@ const makeBackup = () => {
}
let saves = 0
const saveGame = () => {
const saveGame = (force = true) => {
if (saves % 100 === 0) {
makeBackup()
}
saves += 1
if (force || saves % 10 === 0) {
console.log('SAVING GAME')
fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2))
}
}
const maybeNews = say => {
@ -74,9 +77,10 @@ const maybeNews = say => {
const idFromWord = word => {
if (!word?.startsWith('<@') || !word.endsWith('>')) {
return null
}
return getIdFromName(word)
} else {
return word.substring(2, word.length - 1)
}
}
const getSeconds = () => new Date().getTime() / 1000
@ -113,7 +117,7 @@ const parseAll = (str, allNum) => {
return NaN
}
str = str.toLowerCase()?.replace(/,/g, '')
str = str?.toLowerCase()?.replace(/,/g, '') || '1'
switch (str) {
case 'all':
@ -143,12 +147,10 @@ const parseAll = (str, allNum) => {
console.log('STR', str)
if (str.match(/^\d+$/)) {
console.log('parseInt()')
return parseInt(str)
}
if (str.match(/^\d+\.\d+$/)) {
console.log('parseFloat()')
return Math.round(parseFloat(str))
}
@ -185,6 +187,18 @@ const addAchievement = (user, achievementName, say) => {
}, 500)
}
const fuzzyMatcher = string => new RegExp((string?.toLowerCase() || '').split('').join('.*'), 'i')
let knownUsers = {}
const getIdFromName = name => {
const matcher = fuzzyMatcher(name?.toLowerCase())
const found = Object.entries(knownUsers).find(([id, knownName]) => matcher.test(knownName?.toLowerCase()))
if (found) {
return found[0]
}
return null;
}
const getUser = userId => {
if (!users[userId]) {
users[userId] = {
@ -205,20 +219,24 @@ const getUser = userId => {
return users[userId]
}
const addCoins = (user, add) => {
user.coins += add
user.coinsAllTime += add
user.coinsAllTime = Math.floor(user.coinsAllTime)
user.coins = Math.floor(user.coins)
}
const getCoins = userId => {
const user = getUser(userId)
const currentTime = getSeconds()
const lastCheck = user.lastCheck || currentTime
const secondsPassed = currentTime - lastCheck
const increase = getCPS(user) * secondsPassed
user.coins += increase
user.coinsAllTime += increase
user.coins = Math.floor(user.coins)
addCoins(user, getCPS(user) * secondsPassed)
user.lastCheck = currentTime
setHighestCoins(userId)
saveGame()
//saveGame()
return user.coins
}
@ -290,7 +308,8 @@ const singleItemCps = (user, itemName) => {
const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), 1)
// console.log('itemUpgradeCps', itemUpgradeCps)
const userGeneralUpgrades = user.upgrades.general || []
user.upgrades.general ??= []
const userGeneralUpgrades = user.upgrades.general
const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), 1)
// console.log('generalUpgradeCps', generalUpgradeCps)
@ -382,6 +401,41 @@ const addReactions = async ({ app, channelId, timestamp, reactions }) => {
}
}
}
const daysSinceEpoch = () => {
const today = new Date().getTime()
const epoch = new Date(0).getTime()
return Math.floor((today - epoch) / (1000 * 60 * 60 * 24))
}
const dayOfYear = () => {
const date = new Date()
return ((Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000)
}
game.stonkMarket ??= {
lastDay: daysSinceEpoch(),
stonks: {
duk: {
pattern: "duk",
index: 0,
price: 1_410_911_983_728
},
quak: {
pattern: "quak",
index: 0,
price: 5_111_242_778_696
},
honk: {
pattern: "honk",
index: 0,
price: 511_915_144_009
},
}
}
const userHasCheckedQuackgrade = (user, quackGrade) => (user.quackUpgrades?.checked || []).includes(quackGrade)
module.exports = {
saveGame,
makeBackup,
@ -408,5 +462,11 @@ module.exports = {
chaosFilter,
addReactions,
getCompletedSquadgradeNames,
game
game,
dayOfYear,
daysSinceEpoch,
userHasCheckedQuackgrade,
fuzzyMatcher,
addCoins,
setKnownUsers: users => knownUsers = users
}

View File

@ -4,10 +4,10 @@ const port = 3001
const crypto = require('crypto')
const base64 = require('base-64')
const slack = require('../../slack')
const { game: { users } } = require('./utils')
const { game: { users }, getUser, fuzzyMatcher } = require('./utils')
const apiGetUserId = hash => {
return Object.entries(userGetter.users)
return Object.entries(users)
.filter(([id, user]) => user.pwHash === hash)
.map(([id, user]) => id)[0]
}
@ -17,12 +17,21 @@ const makeHash = pw =>
.update(pw)
.digest('hex')
const illegalCommands = ['!', '!b']
const lastCalls = {}
const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
if (illegalCommands.find(command => commandNames.includes(command))) {
commandNames.forEach(name =>
app.get('/' + name.replace(/!/gi, ''), async (req, res) => res.send('Command is illegal over the web api.'))
)
return
}
const route = async (req, res) => {
const say = async msg => res.send(msg)
const say = async msg => res.send(msg + '\n')
try {
const words = ['', ...Object.keys(req.query)]
console.log('INCOMING API CALL:', name, words)
const [commandName, ...args] = words
console.log('INCOMING API CALL:', commandName, words)
const encoded = req.header('Authorization').substring(5)
const decoded = base64.decode(encoded).substring(1)
const event = {
@ -37,8 +46,8 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
console.log(' bad password')
return
}
const lastCall = userGetter.users[event.user].lastApiCall || 0
const secondsBetweenCalls = 5
const lastCall = lastCalls[event.user] || 0
const secondsBetweenCalls = 30
const currentTime = Math.floor(new Date().getTime() / 1000)
if (lastCall + secondsBetweenCalls > currentTime) {
res.status(400)
@ -47,12 +56,21 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
return
}
console.log(` went through for ${slack.users[event.user]}`)
userGetter.users[event.user].lastApiCall = currentTime
lastCalls[event.user] = currentTime
await action({ event, say, words })
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) })
if (!canUse) {
await say(`Command '${words[0]}' not found`)
return
}
await action({ event, say, trueSay: say, words, args, commandName, user, userId: event.user, haunted })
} catch (e) {
console.error(e)
await say(e.stack)
console.error('route error', e)
await say(`Routing error. Make sure you've set up API access with the !setpw command in slack!\n` +
'Then you can use calls like `curl -u ":yourpw" \'http://10.3.0.48:3001/stonks\'`')
}
}
commandNames.forEach(name =>

View File

@ -6,7 +6,7 @@ const getTrivia = async () => axios.get('https://opentdb.com/api.php?amount=10&c
}
})
.then(res => res.data.results)
.catch(console.error)
.catch(e => console.error('trivia error', e))
module.exports = {
getTrivia

View File

@ -1,6 +1,6 @@
const { App: SlackApp } = require('@slack/bolt')
const config = require('../config')
const { addReactions } = require('../games/hvacoins/utils')
const { addReactions, saveGame } = require('../games/hvacoins/utils')
const temperatureChannelId = 'C034156CE03'
@ -27,7 +27,7 @@ try {
console.log('Failed to initialize SlackApp', e)
}
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold']
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater']
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
const sendHelp = async (say, prefix) => {
@ -53,6 +53,7 @@ const getMessage = async ({ channel, ts }) => app.client.conversations.history({
})
app.event('reaction_added', async ({ event, context, client, say }) => {
console.log('reaction_added', event)
for (const listener of reactionListeners) {
listener({ event, say })
}
@ -70,6 +71,8 @@ const users = {
U0X0ZQCN6: 'Caleb',
U03BBTD4CQZ: 'Fernando',
U03DF152WUV: 'Nik',
U2X0SG7BP: 'John',
UR2H5KNHY: 'Jake',
Sage: 'U028BMEBWBV',
Adam: 'U02U15RFK4Y',
@ -81,15 +84,34 @@ const users = {
Caleb: 'U0X0ZQCN6',
Hvacker: 'U0344TFA7HQ',
Fernando: 'U03BBTD4CQZ',
Nik: 'U03DF152WUV'
John: 'U2X0SG7BP',
Jake: 'UR2H5KNHY',
}
const buildSayPrepend = ({ say, prepend }) => async msg => {
if (typeof(msg) === 'string') {
return say(prepend + msg)
}
return say({
...msg,
text: prepend + msg.text
})
}
process.once('SIGINT', code => {
saveGame(true)
process.exit()
})
const activePolls = {}
const testId = 'U028BMEBWBV_TEST'
let testMode = false
app.event('message', async ({ event, context, client, say }) => {
if (event.subtype !== 'message_changed') {
console.log(event)
if (event.subtype !== 'message_changed' && event?.text !== '!') {
console.log('message.event', {
...event,
userName: users[event.user]
})
}
if (event?.user === users.Sage) {
if (event?.text.startsWith('!')) {
@ -116,6 +138,10 @@ app.event('message', async ({ event, context, client, say }) => {
}
if (event.user === users.Sage && event.channel === 'D0347Q4H9FE') {
if (event.text === '!!kill') {
saveGame(true)
process.exit(1)
} else if (event.text === '!!restart') {
saveGame(true)
process.exit()
}
if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) {
@ -167,6 +193,7 @@ app.event('message', async ({ event, context, client, say }) => {
reactCounts[name] += 1
}
})
console.log('REACT COUNTS', JSON.stringify(reactCounts))
const contentVotes = reactCounts[goodEmoji] || 0
let hotterVotes = reactCounts[hotterEmoji] || 0
@ -188,11 +215,11 @@ app.event('message', async ({ event, context, client, say }) => {
let text
if (hotterVotes > colderVotes && hotterVotes > contentVotes) {
text = `<@${users.Quade}> The people have spoken, and would like to `
text = `<@${users.Adam}> The people have spoken, and would like to `
text += 'raise the temperature, quack.'
requestTempChange('Hotter')
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
text = `<@${users.Quade}> The people have spoken, and would like to `
text = `<@${users.Adam}> The people have spoken, and would like to `
text += 'lower the temperature, quack quack.'
requestTempChange('Colder')
} else {
@ -233,7 +260,7 @@ const messageIn = async (channel, optionsOrText) => {
const startPoll = async () => {
const sent = await postToTechThermostatChannel({
text: `<!here|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.` +
'\n(Note that I can\'t actually change the temperature yet. Make Quade do it!)'
})
@ -269,15 +296,15 @@ const decodeData = (key, message) => {
const onReaction = listener => reactionListeners.push(listener)
const channelIsIm = async channel => (await app.client.conversations.info({ channel }))?.channel?.is_im
onReaction(async ({ event }) => {
if (event.user === users.Sage) {
if (event.reaction === 'x') {
if (event.reaction === 'x' && (event.user === users.Sage || await channelIsIm(event.item.channel))) {
try {
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
} catch (e) {
}
}
}
})
module.exports = {
@ -296,5 +323,6 @@ module.exports = {
messageIn,
testMode,
testId,
users
users,
buildSayPrepend
}