Compare commits

..

No commits in common. "969a6b900b2a1f4aa6d79807a983a93447d46bcf" and "e971f7e7c239bab29952cc2b01fddf1d84b6fd61" have entirely different histories.

22 changed files with 725 additions and 9609 deletions

View File

@ -1,20 +0,0 @@
# Hvacker
A Slack-based idle game inspired by (and/or ripping off) Cookie Clicker.
Endlessly self-referential and full of outdated or otherwise 100% unfunny in-jokes, the game has
actually ended up pretty playable. Progression is classic diminishing-returns idler, and a few
social elements keep things from getting stale too quickly.
It also features (among other things) a simple poll system for voting on thermostat controls (hence
the name, HVACker) and was originally intended to control a thermostat directly, but the system on
hand ended up lacking the appropriate API support.
## Design Philosophy
Unlike most game development frameworks, Slack has significant rate-limiting depending on the
actions you want to take. Thus, it was decided that as few elements as possible should actually
update in real time. Instead of processing game ticks as frequently as possible, they are handled
only at request time. Essentially, using a very large delta value between ticks.
This also leaves the Hvacker server relatively lightweight, in an idle state at nearly all times.

5778
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,8 @@ const url = ''
const headers = new Headers() const headers = new Headers()
headers.append('Authorization', 'Basic ' + base64.encode(config.honeywellKey + ':' + config.honeywellSecret)) headers.append('Authorization', 'Basic ' + base64.encode(config.honeywellKey + ':' + config.honeywellSecret))
fetch(url, { fetch(url, {method:'GET',
method: 'GET', headers: headers,
headers: headers //credentials: 'user:passwd'
// credentials: 'user:passwd'
}).then(response => response.json()) }).then(response => response.json())
.then(json => console.log('json', json)) .then(json => console.log(json));

View File

@ -1,12 +1,12 @@
const routine = require('./routine') const routine = require('./routine')
const emptyBoard = [ const emptyBoard = [
[' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', ' ', ' '] [' ', ' ', ' ', ' ', ' ', ' ', ' ',]
] ]
const textFromBoard = board => { const textFromBoard = board => {
@ -54,7 +54,7 @@ const checkDiagonals = board => {
board[row][col] === board[row + 1][col + 1] && board[row][col] === board[row + 1][col + 1] &&
board[row][col] === board[row + 2][col + 2] && board[row][col] === board[row + 2][col + 2] &&
board[row][col] === board[row + 3][col + 3] board[row][col] === board[row + 3][col + 3]
) { ){
return board[row][col] return board[row][col]
} }
} }
@ -65,7 +65,7 @@ const checkDiagonals = board => {
board[row][col] === board[row + 1][col - 1] && board[row][col] === board[row + 1][col - 1] &&
board[row][col] === board[row + 2][col - 2] && board[row][col] === board[row + 2][col - 2] &&
board[row][col] === board[row + 3][col - 3] board[row][col] === board[row + 3][col - 3]
) { ){
return board[row][col] return board[row][col]
} }
} }
@ -76,7 +76,7 @@ const checkDiagonals = board => {
const checkFull = board => { const checkFull = board => {
for (const row of board) { for (const row of board) {
for (const col of row) { for (const col of row) {
if (col === ' ') { if (col !== ' ') {
return null return null
} }
} }

View File

@ -2,12 +2,12 @@ module.exports = {
leaderBoardViewer: { leaderBoardViewer: {
name: 'Leaderboard-Viewer', name: 'Leaderboard-Viewer',
description: 'Thank you for viewing the leaderboard!', description: 'Thank you for viewing the leaderboard!',
emoji: 'trophy' emoji: 'trophy',
}, },
seeTheQuade: { seeTheQuade: {
name: 'See the Quade', name: 'See the Quade',
description: 'Quade has appeared in your buyables', description: 'Quade has appeared in your buyables',
emoji: 'quade' emoji: 'quade',
}, },
greenCoin: { greenCoin: {
name: 'Lucky Green Coin', name: 'Lucky Green Coin',
@ -34,21 +34,6 @@ module.exports = {
description: 'I like big bets, and that\'s the truth', description: 'I like big bets, and that\'s the truth',
emoji: 'slot_machine' 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'
},
sigmaBets: {
name: 'Make a bet over 100 Quintillion',
description: 'Return to monke',
emoji: 'yeknom'
},
ignited: { ignited: {
name: 'You light my fire, baby', name: 'You light my fire, baby',
description: 'And you pay attention to descriptions!', description: 'And you pay attention to descriptions!',
@ -60,79 +45,8 @@ module.exports = {
description: 'I\'m beginning to feel like a rat god, rat god.', description: 'I\'m beginning to feel like a rat god, rat god.',
emoji: 'mouse2' 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'
},
government100: {
name: 'Run 100 Governments',
description: `I hope you're using them for something good...`,
emoji: 'japanese_castle'
},
weAllNeedHelp: { weAllNeedHelp: {
name: 'View the \'!coin\' help', name: `View the '!coin' help`,
description: 'We all need a little help sometimes', description: 'We all need a little help sometimes',
emoji: 'grey_question' emoji: 'grey_question'
}, },
@ -157,25 +71,9 @@ module.exports = {
description: 'You absolutely know how to party.', description: 'You absolutely know how to party.',
emoji: 'sunglasses' emoji: 'sunglasses'
}, },
itsOverNineHundred: {
name: 'Play the HVAC game 1000 times',
description: 'It\'s over nine hundred and ninety-nine!',
emoji: 'chart_with_upwards_trend'
},
youDisgustMe: { youDisgustMe: {
name: 'You disgust me', name: 'You disgust me',
description: 'Like, wow.', description: 'Like, wow.',
emoji: 'nauseated_face' emoji: 'nauseated_face'
},
bookWorm: {
name: 'Take a peek at the lore',
description: 'It\'s 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,17 +1,25 @@
const buyableItems = require('./buyableItems') const buyableItems = require('./buyableItems');
const { commas, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter, fuzzyMatcher, calculateCost } = require('./utils') const { commas, saveGame, setHighestCoins, addAchievement, getCoins, getUser, singleItemCps } = require('./utils');
const slack = require('../../slack') const slack = require('../../slack')
const leaderboardUpdater = {} 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 getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => { const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => {
const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost) const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost)
const itemCps = Math.round(singleItemCps(user, itemName)) const itemCps = Math.round(singleItemCps(user, itemName))
return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(itemCps)} CPS\n_${description}_` return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(itemCps)} CPS\n_${description}_`
} }
const canView = (item, highestCoins) => item.baseCost < (highestCoins || 1) * 101 const canView = highestCoins => ([, item]) => item.baseCost < (highestCoins || 1) * 101
const buyableText = (highestCoins, user) => Object.entries(buyableItems) const buyableText = (highestCoins, user) => Object.entries(buyableItems)
.filter(([, item]) => canView(item, highestCoins)) .filter(canView(highestCoins))
.map(getItemHeader(user)) .map(getItemHeader(user))
.join('\n\n') + .join('\n\n') +
'\n\n:grey_question::grey_question::grey_question:' + '\n\n:grey_question::grey_question::grey_question:' +
@ -21,24 +29,9 @@ const buildBlock = ({ user, itemName, cost, cps }) => ({
type: 'section', type: 'section',
text: { text: {
type: 'mrkdwn', type: 'mrkdwn',
text: `${itemName} :${buyableItems[itemName].emoji}:x${user.items[itemName] || 0} - H${commas(cost)} - ${commas(cps)} CPS\n_${buyableItems[itemName].description}_` text: `${itemName} :${buyableItems[itemName].emoji}:x${user.items[itemName] || 0} - 𝕳${commas(cost)} - ${commas(cps)} CPS\n_${buyableItems[itemName].description}_`
}, },
accessory: { accessory: {
type: 'button',
text: {
type: 'plain_text',
text: '1',
emoji: true
},
value: 'buy_' + itemName,
action_id: 'buy_' + itemName
}
})
const buildBlock2 = ({ user, itemName, cost, cps }) => ({
type: 'actions',
elements: [
{
type: 'button', type: 'button',
text: { text: {
type: 'plain_text', type: 'plain_text',
@ -48,111 +41,72 @@ const buildBlock2 = ({ user, itemName, cost, cps }) => ({
value: 'buy_' + itemName, value: 'buy_' + itemName,
action_id: 'buy_' + itemName action_id: 'buy_' + itemName
}, },
{
type: 'button',
text: {
type: 'plain_text',
text: 'Buy 1',
emoji: true
},
value: 'buy_' + itemName,
action_id: 'buy_' + itemName
}
]
}) })
const buyText2 = (highestCoins, user, extraMessage = '') => { const buyText2 = (highestCoins, user) => {
return ({ return ({
text: (extraMessage && extraMessage + '\n') text: buyableText(highestCoins, user),
+ `You have ${commas(user.coins)} HVAC to spend.\n` blocks: Object.entries(buyableItems)
+ buyableText(highestCoins, user), .filter(canView(highestCoins))
blocks: [ .map(([itemName, item]) => {
{
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 cost = calculateCost({ itemName, user, quantity: 1 })
const cps = Math.round(singleItemCps(user, itemName)) const cps = Math.round(singleItemCps(user, itemName))
return ({ user, itemName, cost, cps }) return ({ user, itemName, cost, cps })
}).map(buildBlock) }).map(buildBlock)
]
}) })
} }
const maxQuantity = ({ itemName, user, currentCoins }) => { const buyRoute = async ({ event, say, words }) => {
let quantity = 1 const user = getUser(event.user)
while (calculateCost({ itemName, user, quantity: quantity + 1 }) <= currentCoins) { const buying = words[1]
quantity++
}
return quantity
}
const buyRoute = async ({ event, say, args, user }) => {
const buying = args[0]
setHighestCoins(event.user) setHighestCoins(event.user)
const query = event?.text?.startsWith('?b ') || event?.text?.startsWith('?buy ')
if (!buying) { if (!buying) {
const highestCoins = user.highestEver || user.coins || 1 const highestCoins = user.highestEver || user.coins || 1
if (canView(buyableItems.quade, highestCoins)) { if (buyableItems.quade.baseCost < highestCoins * 100) {
addAchievement(user, 'seeTheQuade', say) addAchievement(user, 'seeTheQuade', say)
} }
await say(buyText2(highestCoins, user)) await say(buyText2(highestCoins, user))
return return
} }
const matcher = fuzzyMatcher(buying) const buyable = buyableItems[buying]
const buyable = Object.entries(buyableItems).find(([name]) => matcher.test(name))
if (!buyable) { if (!buyable) {
await say('That item does not exist!') await say('That item does not exist!')
return return
} }
const [buyableName, buyableItem] = buyable let quantity = 1
let quantity const currentCoins = getCoins(event.user)
const currentCoins = user.coins if (words[2] === 'max') {
const max = maxQuantity({ itemName: buyableName, user, currentCoins }) while (calculateCost({ itemName: buying, user, quantity: quantity + 1 }) <= currentCoins) {
if (!args[1]) { quantity++
quantity = 1
} else if (args[1] === 'max') {
quantity = max
} else {
if (query) {
quantity = parseInt(args[1])
} else {
quantity = Math.round(chaosFilter(parseInt(args[1]), 0.2, user, max) || 1)
} }
} else {
quantity = parseInt(words[2] || '1')
} }
if (!quantity || quantity < 1) { if (!quantity || quantity < 1) {
await say('Quantity must be a positive integer') await say('Quantity must be a positive integer')
return return
} }
const realCost = calculateCost({ itemName: buyableName, user, quantity }) const realCost = calculateCost({ itemName: buying, user, quantity })
if (query) {
return say(`Buying ${quantity} ${buyableName} would cost you ${commas(realCost)} HVAC`)
}
if (currentCoins < realCost) { if (currentCoins < realCost) {
await say(`You don't have enough coins! You need ${commas(realCost)}`) await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`)
return return
} }
user.coins -= realCost user.coins -= realCost
user.items[buyableName] = user.items[buyableName] || 0 user.items[buying] = user.items[buying] || 0
user.items[buyableName] += quantity user.items[buying] += quantity
console.log(buying, user.items.mouse)
if (user.items[buyableName] >= 100) { if (buying === 'mouse' && user.items.mouse >= 100) {
addAchievement(user, buyableItems[buyableName].own100Achievement, say) addAchievement(user, 'ratGod', say)
} }
if (quantity === 1) {
const countString = quantity === 1 ? 'one' : quantity await say(`You bought one :${buyable.emoji}:`)
await say(`You bought ${countString} :${buyableItem.emoji}:`) } else {
await say(`You bought ${quantity} :${buyable.emoji}:`)
}
saveGame()
} }
const buyButton = async ({ body, ack, say, payload }) => { const buyButton = async ({ body, ack, say, payload }) => {
@ -160,25 +114,19 @@ const buyButton = async ({ body, ack, say, payload }) => {
const buying = payload.action_id.substring(4) const buying = payload.action_id.substring(4)
console.log(`buyButton ${buying} clicked`) console.log(`buyButton ${buying} clicked`)
const event = { const event = {
user: body.user.id user: body.user.id,
} }
const user = getUser(event.user) const user = getUser(event.user)
const words = ['', buying, body.actions[0].text] const words = ['', buying, '1']
const [commandName, ...args] = words await buyRoute({ event, say, words })
let extraMessage = ''
say = async text => extraMessage = text
await buyRoute({ event, say, words, args, commandName, user })
const highestCoins = user.highestEver || user.coins || 1 const highestCoins = user.highestEver || user.coins || 1
await slack.app.client.chat.update({ await slack.app.client.chat.update({
channel: body.channel.id, channel: body.channel.id,
ts: body.message.ts, ts: body.message.ts,
...buyText2(highestCoins, user, extraMessage) ...buyText2(highestCoins, user)
}) })
await leaderboardUpdater.updateAllLeaderboards()
} }
Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton)) Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton))
module.exports = { buyRoute, leaderboardUpdater } module.exports = buyRoute

View File

@ -3,105 +3,72 @@ module.exports = {
baseCost: 100, baseCost: 100,
earning: 1, earning: 1,
emoji: 'mouse2', emoji: 'mouse2',
description: 'A mouse to steal coins for you.', description: 'A mouse to steal coins for you.'
own100Achievement: 'ratGod',
}, },
accountant: { accountant: {
baseCost: 1_100, baseCost: 1_100,
earning: 8, earning: 8,
emoji: 'male-office-worker', emoji: 'male-office-worker',
description: 'Legally make money from nothing!', description: 'Legally make money from nothing!'
own100Achievement: 'mathematician',
}, },
whale: { whale: {
baseCost: 12_000, baseCost: 12_000,
earning: 47, earning: 47,
emoji: 'whale', 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: { train: {
baseCost: 130_000, baseCost: 130_000,
earning: 260, earning: 260,
emoji: 'train2', emoji: 'train2',
description: 'Efficiently ship your most valuable coins.', description: 'Efficiently ship your most valuable coins.'
own100Achievement: 'train100',
}, },
fire: { fire: {
baseCost: 1_400_000, baseCost: 1_400_000,
earning: 1_400, earning: 1_400,
emoji: 'fire', emoji: 'fire',
description: 'Return to the roots of HVAC.', description: 'Return to the roots of HVAC.'
own100Achievement: 'fire100',
}, },
boomerang: { boomerang: {
baseCost: 20_000_000, baseCost: 20_000_000,
earning: 7_800, earning: 7_800,
emoji: 'boomerang', emoji: 'boomerang',
description: 'Your coin always seems to come back.', description: 'Your coin always seems to come back.'
own100Achievement: 'boom100',
}, },
moon: { moon: {
baseCost: 330_000_000, baseCost: 330_000_000,
earning: 44_000, earning: 44_000,
emoji: 'new_moon_with_face', emoji: 'new_moon_with_face',
description: 'Convert dark new-moon energy into HVAC Coins.', description: 'Convert dark new-moon energy into HVAC Coins.'
own100Achievement: 'moon100',
}, },
butterfly: { butterfly: {
baseCost: 5_100_000_000, baseCost: 5_100_000_000,
earning: 260_000, earning: 260_000,
emoji: 'butterfly', 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: { mirror: {
baseCost: 75_000_000_000, baseCost: 75_000_000_000,
earning: 1_600_000, earning: 1_600_000,
emoji: 'mirror', emoji: 'mirror',
description: 'Only by gazing inward can you collect enough Coin to influence the thermostat.', description: 'Only by gazing inward can you collect enough Coin to influence the thermostat.'
own100Achievement: 'mirror100',
}, },
quade: { quade: {
baseCost: 1_000_000_000_000, baseCost: 1_000_000_000_000,
earning: 10_000_000, earning: 10_000_000,
emoji: 'quade', emoji: 'quade',
description: 'Has thumbs capable of physically manipulating the thermostat.', description: 'Has thumbs capable of physically manipulating the thermostat.'
own100Achievement: 'quade100',
}, },
hvacker: { hvacker: {
baseCost: 14_000_000_000_000, baseCost: 14_000_000_000_000,
earning: 65_000_000, earning: 65_000_000,
emoji: 'hvacker_angery', emoji: 'hvacker_angery',
description: 'Harness the power of the mad god himself.', description: 'Harness the power of the mad god himself.'
own100Achievement: 'hvacker100',
}, },
creator: { creator: {
baseCost: 170_000_000_000_000, baseCost: 170_000_000_000_000,
earning: 430_000_000, earning: 430_000_000,
emoji: 'question', emoji: 'question',
description: 'The elusive creator of Hvacker takes a favorable look at your CPS.', description: 'The elusive creator of Hvacker takes a favorable look at your CPS.'
own100Achievement: '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: '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: '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

@ -1,174 +0,0 @@
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,113 +0,0 @@
const { addAchievement, saveGame, getUser } = require('./utils')
const slack = require('../../slack')
let loreCount = 0
const l = (text, {correctReactions, correctResponse, incorrectResponse, jumpTo} = {}) => {
loreCount += 1
return {
text,
correctReactions,
correctResponse,
incorrectResponse,
jumpTo
}
}
const lore = [
l(`Allow me to tell you a story.`),
l(`Once upon a time, there were two young ducks named Genevieve and Isaiah.`),
l(`Isaiah was known mostly for being a big butthole whomst no one liked.`),
l(`Genevieve was known for a singing voice that could peel the paint off your car.`),
l(`Nevertheless, there's was a passionate love affair.`),
l(`_Honking_`),
l(`...`),
l(`Hey you know, it's rather cold, don't you think? Could you start a fire for us?`, {
correctReactions: ['fire'],
correctResponse: `Thank you.`,
incorrectResponse: `Well, you're looking in the right place, anyway.`,
}),
l(`Anyway, together they laid nine eggs.`),
l(`The first to hatch was Rick, who grew up into a fine bird with beautiful feathers.`),
l(`The second was Claire, who grew into an even finer bird, glowing with Duckish beauty.`),
l(`The third was Marf, who developed a severe addiction to Elmer's glue.`),
l(`Bird four was Yoink, who considered Tupperware a hobby.`),
l(`The fifth was Big Bob.`),
l(`The sixth was Jess, who became the number one checkers player in the world.`),
l(`The seventh was Small Bob.`),
l(`The eighth was Maurice, who had quite a few words to say about the French, and was eventually elected president.`),
l(`And the ninth...`),
l(`Well, the ninth might actually amount to something.`),
l(`https://i.imgur.com/eFreg7Y.gif\n`),
]
slack.onReaction(async ({ event, say }) => {
try {
const user = getUser(event.user)
const item = await slack.getMessage({ channel: event.item.channel, ts: event.item.ts })
const message = item.messages[0].text
const loreData = slack.decodeData('lore', message)
if (!loreData || user.lore !== loreData.index || !loreData.correctReactions) {
return
}
if (!loreData.correctReactions.includes(event.reaction)) {
if (lore[user.lore].incorrectResponse) {
await say(lore[user.lore].incorrectResponse + encodeLore(user.lore))
}
return
}
console.log('lore:', lore[user.lore])
await say(lore[user.lore].correctResponse)
user.lore += 1
saveGame(`updating ${user.name}'s lore counter`)
} catch (e) {console.error('onReaction error', e)}
})
const encodeLore = loreNumber => lore[loreNumber].text.startsWith(':') && lore[loreNumber].text.endsWith(':') ? '' :
slack.encodeData('lore', {
index: loreNumber,
correctReactions: lore[loreNumber].correctReactions
})
const loreMessage = (user, say) => {
if (lore[user.lore]) {
return lore[user.lore].text + encodeLore(user.lore)
}
addAchievement(user, 'bookWorm', say)
return `Sorry. I'd love to tell you more, but I'm tired. Please check back later.`
}
const loreRoute = async ({ say, args, user, isAdmin }) => {
user.lore ??= 0
if (!args[0]) {
const message = loreMessage(user, say)
await say(message)
if (!lore[user.lore]?.correctReactions) {
user.lore += 1
}
//saveGame()
console.log('Sent ' + user.name + ':\n' + message)
return
}
if (args[0] === 'reset') {
user.lore = 0
//saveGame()
return say(`I have reset your place in the story.`)
}
if (isAdmin) {
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(args[0])
if (!isNaN(jumpTo)) {
user.lore = jumpTo
//saveGame()
}
}
}
module.exports = loreRoute

View File

@ -1,54 +1,56 @@
const { commas, quackGradeMultiplier, prestigeMultiplier, makeBackup, userHasCheckedQuackgrade, getUser } = require('./utils') const { getUser, getCoins, commas, saveGame } = require('./utils');
const { quackStore } = require('./quackstore') const quackStore = require('./quackstore');
const buyableItems = require('./buyableItems')
const slack = require('../../slack')
const possiblePrestige = coins => { const possiblePrestige = coins => {
let p = 0 let p = 0
while (totalCostForPrestige(p + 1) <= coins) { while (tpcRec(p + 1) <= coins) {
p += 1 p += 1
} }
return p return p
} }
const tpcRecMemo = []
const totalCostForPrestige = prestigeLevel => { const totalCostForPrestige = prestigeLevel => {
let cost = 0
while (prestigeLevel) {
cost += 1_000_000_000_000 * Math.pow(prestigeLevel, 3)
prestigeLevel -= 1
}
return cost
}
const tpcRecMemo = []
const tpcRec = prestigeLevel => {
if (prestigeLevel === 0) { if (prestigeLevel === 0) {
return 0 return 0
} }
return (tpcRecMemo[prestigeLevel]) || (tpcRecMemo[prestigeLevel] = 1_000_000_000_000 * Math.pow(prestigeLevel, 3) + totalCostForPrestige(prestigeLevel - 1)) return (tpcRecMemo[prestigeLevel]) || (tpcRecMemo[prestigeLevel] = 1_000_000_000_000 * Math.pow(prestigeLevel, 3) + tpcRec(prestigeLevel - 1))
} }
const prestigeRoute = async ({ say, args, user }) => { // TODO
const prestigeRoute = async ({ event, say, words }) => {
const user = getUser(event.user)
getCoins(event.user)
const possible = possiblePrestige(user.coinsAllTime) const possible = possiblePrestige(user.coinsAllTime)
const current = user.prestige ??= 0 const current = user.prestige ??= 0
if (args[0] === 'me') { if (words[1] === 'me') {
await say( await say(
'This will permanently remove all of your items, upgrades, and coins!\n\n' + 'This will permanently remove all of your items, upgrades, and coins!\n\n' +
'Say \'!!prestige me\' to confirm.' 'Say \'!!prestige me\' to confirm.'
) )
} else { } else {
const currentCost = totalCostForPrestige(possible)
const nextCost = totalCostForPrestige(possible + 1)
const diff = nextCost - currentCost
const progress = user.coinsAllTime - currentCost
const bars = Math.round((progress / diff) * 10)
const empty = 10 - bars
const progressBar = '[' + '='.repeat(bars) + ' '.repeat(empty) + ']'
await say( await say(
`Current Prestige: ${commas(current)}\n\n` + `Current Prestige: ${commas(current)}\n\n` +
`Quacks gained if you prestige now: ${commas(possible - current)}\n\n` + `Quacks gained if you prestige now: ${commas(possible - current)}\n\n` +
`Next quack progress: \`${progressBar} ${commas(diff)} \`\n\n` + `HVAC until next quack: ${commas(Math.round(tpcRec(possible + 1) - user.coinsAllTime))}\n\n` +
'Say \'!prestige me\' to start the prestige process.' + 'Say \'!prestige me\' to start the prestige process.'
`\n\nYour prestige is currently boosting your CPS by ${commas((prestigeMultiplier(user) - 1) * 100)}%`
) )
} }
}//, true, adminOnly) }//, true, adminOnly)
const prestigeConfirmRoute = async ({ event, say, user, YEET }) => { // TODO
if (YEET) { const prestigeConfirmRoute = async ({ event, say, words }) => {
return say(prestigeMenu(user)) const user = getUser(event.user)
} getCoins(event.user)
const possible = possiblePrestige(user.coinsAllTime) const possible = possiblePrestige(user.coinsAllTime)
const current = user.prestige const current = user.prestige
if (possible <= current) { if (possible <= current) {
@ -59,243 +61,65 @@ const prestigeConfirmRoute = async ({ event, say, user, YEET }) => {
await say('Say exactly \'!!prestige me\' to confirm') await say('Say exactly \'!!prestige me\' to confirm')
return return
} }
await makeBackup() console.log('possible', possible)
console.log('user.prestige', user.prestige)
user.isPrestiging = true
user.quacks ??= 0 user.quacks ??= 0
user.quacks += (possible - user.prestige) user.quacks += (possible - user.prestige)
console.log('user.quacks', user.quacks)
user.prestige = possible user.prestige = possible
user.highestEver = 0
user.coins = 0 user.coins = 0
user.items = {} user.items = {}
user.holdings = {}
const starterUpgrades = (user.quackUpgrades?.starter || [])
starterUpgrades.forEach(upgradeName => quackStore[upgradeName].effect(user))
user.upgrades = {} user.upgrades = {}
saveGame()
await say('You prestiged! Check out !quackstore to see what you can buy!')
}
await say(prestigeMenu(user)) const quackStoreListing = ([name, upgrade]) =>
await say(`Say !quack _upgrade-name_ to purchase new quackgrades!`) `:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
//await say('You prestiged! Check out !quackstore to see what you can buy!')
}
const quackStoreListing = (showCost = true) => ([name, upgrade]) =>
`:${upgrade.emoji}: *${name}* - ${showCost ? 'Costs' : 'Worth'} *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
const allUserQuackUpgrades = user => const allUserQuackUpgrades = user =>
Object.entries(user.quackUpgrades || {}) Object.entries(user.quackUpgrades || {})
.map(([type, upgrades]) => upgrades).flatMap(x => x) .map(([type, upgrades]) => upgrades)
const hasPreReqs = user => ([name, upgrade]) => { const hasPreReqs = user => ([name, upgrade]) => {
if (!upgrade.preReqs) { if (!upgrade.preReqs) {
return true return true
} }
const allUserUpgrades = allUserQuackUpgrades(user) const allUserUpgrades = allUserQuackUpgrades(user)
console.log(allUserUpgrades)
return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq)) return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq))
} }
const owns = (user, [name, upgrade]) => allUserQuackUpgrades(user).includes(name)
const ownedQuackItems = user => Object.entries(quackStore).filter(upgrade => owns(user, upgrade))
const unownedQuackItems = user => Object.entries(quackStore).filter(upgrade => !owns(user, upgrade))
const quackStoreText = user => const quackStoreText = user =>
unownedQuackItems(user) Object.entries(quackStore)
.filter(hasPreReqs(user)) .filter(hasPreReqs(user))
.map(quackStoreListing(true)) .map(quackStoreListing)
.join('\n\n') + .join('\n\n')
`\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, YEET }) => { const quackStoreRoute = async ({ event, say, words }) => {
const user = getUser(event.user)
user.quackUpgrades ??= {} user.quackUpgrades ??= {}
if (!args[0] || !YEET) { const quacks = user.quacks ??= 0
if (!words[1]) {
await say(quackStoreText(user)) await say(quackStoreText(user))
return return
} }
const quackItem = quackStore[args[0]] const quackItem = quackStore[words[1]]
if (!quackItem || !unownedQuackItems(user).find(([name]) => name === args[0])) { if (!quackItem) {
await say(`'${args[0]}' is not available in the quack store!`) await say(`'${words[1]}' is not available in the quack store!`)
return return
} }
const quacks = user.quacks ??= 0
if (quackItem.cost > quacks) { if (quackItem.cost > quacks) {
await say(`${args[0]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`) await say(`${words[1]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
return return
} }
user.quacks -= quackItem.cost
user.quackUpgrades[quackItem.type] ??= [] user.quackUpgrades[quackItem.type] ??= []
user.quackUpgrades[quackItem.type].push(args[0]) user.quackUpgrades[quackItem.type].push(words[1])
if (quackItem.type === 'starter') { saveGame()
quackItem.effect(user)
}
await say(`You bought ${args[0]}!`)
}
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 =>
ownedQuackItems(user)
.filter(hasPreReqs(user))
.map(quackStoreListing(false))
.join('\n\n') +
`\n\nQuackStore upgrades are currently boosting your CPS by ${commas((quackGradeMultiplier(user) - 1) * 100)}%`
const ownedQuacksRoute = async ({ say, user }) => {
user.quackUpgrades ??= {}
user.quacks ??= 0
await say(ownedQuacksText(user))
} }
module.exports = { module.exports = {
quackStoreRoute, quackStoreRoute,
prestigeRoute, prestigeRoute,
prestigeConfirmRoute, prestigeConfirmRoute
prestigeMenuRoute,
ownedQuacksRoute
} }

View File

@ -1,10 +1,3 @@
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
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) + offset) % chaosCpsMods.length]
const quackStore = { const quackStore = {
ascent: { ascent: {
name: 'Ascent', name: 'Ascent',
@ -18,159 +11,10 @@ const quackStore = {
name: 'Nuclear Fuel', name: 'Nuclear Fuel',
type: 'cps', type: 'cps',
emoji: 'atom_symbol', emoji: 'atom_symbol',
description: 'The future is now, old man. Boosts all CPS by 20%.', description: 'The future is now. Boosts all CPS by 20%.',
preReqs: ['ascent'], preReqs: ['ascent'],
effect: cps => cps * 1.2, effect: cps => cps * 1.2,
cost: 5 cost: 5
}, },
chaos: {
name: 'Chaos',
type: 'cps',
emoji: 'eye',
description: 'Awaken. Gives a random modifier to your CPS every six seconds. May have other consequences...',
//+ '_\n_Averages a 26% CPS boost.',
preReqs: ['nuclearFuel'],
effect: (cps, user) => {
return cps * getChaos(Math.round(user.interactions / 50))
},
cost: 10
},
dryerSheet: {
name: 'Dryer Sheet',
type: 'lightning',
emoji: 'rose',
description: 'Smells nice. Makes lightning twice as likely to strike.',
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: ['ascent'],
effect: user => {
user.items.mouse ??= 0
user.items.mouse += 5
},
cost: 4
},
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: 16
},
sharkBoy: {
name: 'Shark Boy',
type: 'starter',
emoji: 'ocean',
description: 'Start each prestige with 5 whales',
preReqs: ['silverSpoon'],
effect: user => {
user.items.whale ??= 0
user.items.whale += 5
},
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 = {
quackStore,
getChaos: user => getChaos(user.interactions || 0)
} }
module.exports = quackStore

View File

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

View File

@ -1,666 +1,227 @@
const { getCPS, setUpgrades } = require('./utils'); const basic = ({ type, description, count, cost }) => ({
const basic = ({ name, type, description, count, cost, extraCondition = () => true, effect = cps => cps * 2 }) => ({
name,
type, type,
description, description,
condition: (user, squadGrades) => user.items[type] >= count && extraCondition(user, squadGrades), condition: user => user.items[type] >= count,
cost, cost,
effect effect: itemCps => itemCps * 2
})
const evil = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 40,
cost,
extraCondition: (user, squadGrades) => squadGrades?.includes('discardHumanMorals'),
})
const heavenly = ({ name, type, description, cost, multiplier = 2 }) => ({
name,
type,
description,
condition: (user, squadGrades) => user.items[type] >= 60 && squadGrades?.includes('redemption'),
cost,
effect: cps => cps * multiplier
})
const disabled = () => false
const baby = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 70,
cost,
extraCondition: disabled
})
const geometry = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 100,
cost,
extraCondition: disabled
})
const universitality = ({ name, type, description, cost }) => basic({
name,
type,
description,
count: 100,
cost,
extraCondition: disabled
}) })
module.exports = { module.exports = {
doubleClick: basic({ doubleClick: basic({
name: 'Double-Click',
type: 'mouse', type: 'mouse',
description: 'Doubles the power of mice', description: 'Doubles the power of mice',
count: 1, count: 1,
cost: 1_000 cost: 1_000,
}), }),
stinkierCheese: basic({ stinkierCheese: basic({
name: 'Stinkier Cheese',
type: 'mouse', type: 'mouse',
description: 'Mice are doubly motivated to hunt down HVAC Coins', description: 'Mice are doubly motivated to hunt down HVAC Coins',
count: 10, count: 10,
cost: 21_000 cost: 21_000,
}), }),
biggerTeeth: basic({ biggerTeeth: basic({
name: 'Bigger Teeth',
type: 'mouse', type: 'mouse',
description: 'Mice can intimidate twice as much HVAC out of their victims.', description: 'Mice can intimidate twice as much HVAC out of their victims.',
count: 25, count: 25,
cost: 50_000 cost: 50_000,
}),
rats: evil({
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({ fasterComputers: basic({
name: 'Faster Computers',
type: 'accountant', type: 'accountant',
description: 'Accountants can ~steal~ optimize twice as much HVAC!', description: 'Accountants can ~steal~ optimize twice as much HVAC!',
count: 1, count: 1,
cost: 11_000, cost: 11_000,
}), }),
lackOfMorality: basic({ lackOfMorality: basic({
name: 'Lack of Morality',
type: 'accountant', type: 'accountant',
description: 'Accountants are taking a hint from nearby CEOs.', description: 'Accountants are taking a hint from nearby CEOs.',
count: 10, count: 10,
cost: 200_000, cost: 200_000,
}), }),
widerBrains: basic({ widerBrains: basic({
name: 'Wider Brains',
type: 'accountant', type: 'accountant',
description: 'For accountant do double of thinking.', description: 'For accountant do double of thinking.',
count: 25, count: 25,
cost: 550_000, cost: 550_000,
}), }),
vastLayoffs: evil({
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({ biggerBlowhole: basic({
name: 'Bigger Blowhole',
type: 'whale', type: 'whale',
description: 'With all that extra air, whales have double power!', description: 'With all that extra air, whales have double power!',
count: 1, count: 1,
cost: 120_000 cost: 120_000,
}), }),
sassyWhales: basic({ sassyWhales: basic({
name: 'Sassy Whales',
type: 'whale', type: 'whale',
description: 'These are the kind of whales that know how to get twice as much done', description: 'These are the kind of whales that know how to get twice as much done',
count: 10, count: 10,
cost: 3_000_000 cost: 3_000_000,
}), }),
thinnerWater: basic({ thinnerWater: basic({
name: 'Thinner Water',
type: 'whale', type: 'whale',
description: 'Whales can move twice as quickly through this physics-defying liquid', description: 'Whales can move twice as quickly through this physics-defying liquid',
count: 25, count: 25,
cost: 6_000_000 cost: 6_000_000,
}),
blightWhales: evil({
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({ greasyTracks: basic({
name: 'Greasy Tracks',
type: 'train', type: 'train',
description: 'Lets trains deliver HVAC twice as efficiently', description: 'Lets trains deliver HVAC twice as efficiently',
count: 1, count: 1,
cost: 1_300_000 cost: 1_300_000,
}), }),
rocketThrusters: basic({ rocketThrusters: basic({
name: 'Rocket Thrusters',
type: 'train', type: 'train',
description: 'That\'ll put some quack on your track', description: 'That\'ll put some quack on your track',
count: 10, count: 10,
cost: 22_000_000 cost: 22_000_000,
}), }),
loudConductors: basic({ loudConductors: basic({
name: 'Loud Conductors',
type: 'train', type: 'train',
description: 'Conductors can onboard twice as much HVAC', description: 'Conductors can onboard twice as much HVAC',
count: 25, count: 25,
cost: 65_000_000 cost: 65_000_000,
}),
hellTrain: evil({
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,
cost: 2_220_000_000
}), }),
gasolineFire: basic({ gasolineFire: basic({
name: 'Gasoline Fire',
type: 'fire', type: 'fire',
description: 'Extremely good for breathing in.', description: 'Extremely good for breathing in.',
count: 1, count: 1,
cost: 14_000_000 cost: 14_000_000,
}), }),
extremelyDryFuel: basic({ extremelyDryFuel: basic({
name: 'Extremely Dry Fuel',
type: 'fire', type: 'fire',
description: 'Hey, psst, hey. Use the ignite command for a secret achievement.', description: 'Use the ignite command for a secret achievement.',
count: 10, count: 10,
cost: 163_000_000 cost: 163_000_000,
}), }),
cavemanFire: basic({ cavemanFire: basic({
name: 'Caveman Fire',
type: 'fire', type: 'fire',
description: 'They just don\'t make \'em like they used to.', description: 'They just don\'t make \'em like they used to.',
count: 25, count: 25,
cost: 700_000_000 cost: 700_000_000,
}),
lava: evil({
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({ spoonerang: basic({
name: 'Spoonerang',
type: 'boomerang', type: 'boomerang',
description: 'Scoops up HVAC mid-flight', description: 'Scoops up HVAC mid-flight',
count: 1, count: 1,
cost: 200_000_000 cost: 200_000_000,
}), }),
boomerAng: basic({ boomerAng: basic({
name: 'Boomer-ang',
type: 'boomerang', type: 'boomerang',
description: 'It\'s... old.', description: 'It\'s... old.',
count: 10, count: 10,
cost: 1_200_000_000 cost: 1_200_000_000,
}), }),
doubleRang: basic({ doubleRang: basic({
name: 'Double-rang',
type: 'boomerang', type: 'boomerang',
description: 'You throw one, but somehow catch two', description: 'You throw one, but somehow catch two',
count: 25, count: 25,
cost: 10_000_000_000 cost: 10_000_000_000,
}),
loyalRang: evil({
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,
cost: 360_000_000_000
}), }),
lunarPower: basic({ lunarPower: basic({
name: 'Lunar Power',
type: 'moon', type: 'moon',
description: 'Out with the sol, in with the lun!', description: 'Out with the sol, in with the lun!',
count: 1, count: 1,
cost: 3_300_000_000 cost: 3_300_000_000,
}), }),
womanOnTheMoon: basic({ womanOnTheMoon: basic({
name: 'Woman on the Moon',
type: 'moon', type: 'moon',
description: 'There\'s no reason for it not to be a woman!', description: 'There\'s no reason for it not to be a woman!',
count: 10, count: 10,
cost: 39_700_000_000 cost: 39_700_000_000,
}), }),
doubleCraters: basic({ doubleCraters: basic({
name: 'Double-Craters',
type: 'moon', type: 'moon',
description: 'Making every side look like the dark side.', description: 'Making every side look like the dark side.',
count: 25, count: 25,
cost: 165_000_000_000 cost: 165_000_000_000,
}),
tidalUpheaval: evil({
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,
cost: 5_190_000_000_000
}), }),
glassButterfly: basic({ glassButterfly: basic({
name: 'Glass Butterfly',
type: 'butterfly', type: 'butterfly',
description: 'Not your grandma\'s universe manipulation.', description: 'Not your grandma\'s universe manipulation.',
count: 1, count: 1,
cost: 51_000_000_000 cost: 51_000_000_000,
}), }),
monarchMigration: basic({ monarchMigration: basic({
name: 'Monarch Migration',
type: 'butterfly', type: 'butterfly',
description: 'This upgrade brought to you by milkweed.', description: 'This upgrade brought to you by milkweed.',
count: 10, count: 10,
cost: 870_000_000_000 cost: 870_000_000_000,
}), }),
quadWing: basic({ quadWing: basic({
name: 'Quad-Wing',
type: 'butterfly', type: 'butterfly',
description: 'Sounds a lot like a trillion bees buzzing inside your head.', description: 'Sounds a lot like a trillion bees buzzing inside your head.',
count: 25, count: 25,
cost: 2_550_000_000_000 cost: 2_550_000_000_000,
}),
venomousMoths: evil({
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,
cost: 75_300_000_000_000
}), }),
silverMirror: basic({ silverMirror: basic({
name: 'Silver Mirror',
type: 'mirror', type: 'mirror',
description: 'Excellent for stabbing vampires.', description: 'Excellent for stabbing vampires.',
count: 1, count: 1,
cost: 750_000_000_000 cost: 750_000_000_000,
}),
pocketMirror: basic({
name: 'Pocket Mirror',
type: 'mirror',
description: 'Take your self-reflection on the go!',
count: 10,
cost: 18_000_000_000_000
}), }),
window: basic({ window: basic({
name: 'Window',
type: 'mirror', type: 'mirror',
description: 'Only through looking around you can you acquire the self reflection necessary to control the thermostat.', description: 'Only through looking around you can you acquire the self reflection necessary to control the thermostat.',
count: 25, count: 25,
cost: 37_500_000_000_000 cost: 37_500_000_000_000,
}),
crackedMirror: evil({
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,
cost: 1_330_000_000_000_000
}), }),
fzero: basic({ fzero: basic({
name: 'F-Zero',
type: 'quade', type: 'quade',
description: 'Brings out his competitive spirit.', description: 'Brings out his competitive spirit.',
count: 1, count: 1,
cost: 10_000_000_000_000 cost: 10_000_000_000_000,
}),
triHumpCamel: basic({
name: 'Trimedary Camel',
type: 'quade',
description: 'YEE HAW :trimedary_camel:',
count: 10,
cost: 200_000_000_000_000
}), }),
adam: basic({ adam: basic({
name: 'Adam',
type: 'quade', type: 'quade',
description: 'He could probably reach the thermostat if he wanted.', description: 'He could probably reach the thermostat if he wanted.',
count: 25, count: 25,
cost: 500_000_000_000_000 cost: 500_000_000_000_000,
}),
thatsNotQuade: evil({
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,
cost: 18_000_000_000_000_000
}), }),
latestNode: basic({ latestNode: basic({
name: 'Latest Node',
type: 'hvacker', type: 'hvacker',
description: 'The old one has terrible ergonomics, tsk tsk.', description: 'The old one has terrible ergonomics, tsk tsk.',
count: 1, count: 1,
cost: 140_000_000_000_000 cost: 140_000_000_000_000,
}),
nativeFunctions: basic({
name: 'Native Functions',
type: 'hvacker',
description: 'Sometimes javascript just isn\'t fast enough.',
count: 10,
cost: 3_300_000_000_000_000
}), }),
gitCommits: basic({ gitCommits: basic({
name: 'Git Commits',
type: 'hvacker', type: 'hvacker',
description: 'The heads of multiple people in a company are better than, for example, merely one head.', description: 'The heads of multiple people in a company are better than, for example, merely one head.',
count: 25, count: 25,
cost: 7_000_000_000_000_000 cost: 7_000_000_000_000_000,
}),
undefinedBehavior: evil({
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,
cost: 250_000_000_000_000_000
}),
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,
cost: 3_600_000_000_000_000_000
}),
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,
cost: 5_140_000_000_000_000_000
}),
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: { homage: {
name: 'Homage',
type: 'general', type: 'general',
description: 'The power of original ideas increases your overall CPS by 10%', description: 'The power of original ideas increases your overall CPS by 10%',
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 200, condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 200,
emoji: 'cookie', emoji: 'cookie',
cost: 10_000_000_000, cost: 10_000_000_000,
effect: (itemCps, user) => itemCps * 1.1 effect: (itemCps, user) => Math.ceil(itemCps * 1.1)
}, },
iLoveHvac: { iLoveHvac: {
name: 'iLoveHvac',
type: 'general', type: 'general',
description: 'The power of love increases your overall CPS by 10%', description: 'The power of love increases your overall CPS by 10%',
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400, condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
emoji: 'heart', emoji: 'heart',
cost: 100_000_000_000_000, cost: 100_000_000_000_000,
effect: (itemCps, user) => itemCps * 1.1 effect: (itemCps, user) => Math.ceil(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)
} }
// moreUpgrades: {
// type: 'general',
// description: 'Adds additional upgrades',
// condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
// emoji: 'cookie',
// cost: 10_000_000_000_000,
// effect: nothing
// },
} }
setUpgrades(module.exports)

View File

@ -1,79 +1,37 @@
const fs = require('fs') const fs = require('fs')
//const jokes = require('../jokes') const jokes = require('../jokes')
const achievements = require('./achievements') const achievements = require('./achievements');
const buyableItems = require('./buyableItems') const buyableItems = require("./buyableItems");
const { quackStore, getChaos } = require('./quackstore') const upgrades = require("./upgrades");
const quackStore = require("./quackstore");
let upgrades
const setUpgrades = upg => {
upgrades = upg
}
const saveFile = 'hvacoins.json' const saveFile = 'hvacoins.json'
const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ } const logError = msg => msg ? console.error(msg) : () => { /* Don't log empty message */ }
const loadGame = () => { const loadGame = () => parseOr(fs.readFileSync('./' + saveFile, 'utf-8'),
const game = parseOr(fs.readFileSync('./' + saveFile, 'utf-8'),
() => ({ () => ({
users: {}, users: {},
nfts: [], nfts: [],
squad: {}, squad: {}
horrors: {}
})) }))
game.horrors ??= {}
return game
}
const chaosFilter = (num, odds, user, max = Infinity, min = -Infinity) => { const parseOr = (parseable, orFunc) => {
const userQuackgrades = user.quackUpgrades?.cps || []
const hasChaos = userQuackgrades.includes('chaos')
if (!hasChaos || Math.random() < odds || !num) {
return num
}
const chaosed = num * getChaos(user)
if (chaosed > max) {
return max
}
if (chaosed < min) {
return min
}
return chaosed
}
const parseOr = (parseable, fallback) => {
try { try {
if (typeof parseable === 'function') {
parseable = parseable()
}
return JSON.parse(parseable) return JSON.parse(parseable)
} catch (e) { } catch (e) {
logError(e) logError(e)
return fallback() return orFunc()
} }
} }
const makeBackup = () => {
const fileName = './backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_')
console.log(`Making backup file: ${fileName}`)
fs.writeFileSync(fileName, JSON.stringify(game))
}
let saves = 0 let saves = 0
const saveGame = (after, force = true) => { const saveGame = () => {
if (saves % 20 === 0) { if (saves % 100 === 0) {
makeBackup() fs.writeFileSync('./backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_'), JSON.stringify(game))
} }
saves += 1 saves += 1
if (force || saves % 10 === 0) {
if (after) {
console.log(`SAVING GAME after ${after}`)
} else {
console.log('SAVING GAME')
}
fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2)) fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2))
}
} }
const maybeNews = say => { const maybeNews = say => {
@ -89,116 +47,14 @@ const maybeNews = say => {
const idFromWord = word => { const idFromWord = word => {
if (!word?.startsWith('<@') || !word.endsWith('>')) { if (!word?.startsWith('<@') || !word.endsWith('>')) {
return getIdFromName(word) return null
} else {
return word.substring(2, word.length - 1)
} }
return word.substring(2, word.length - 1)
} }
const getSeconds = () => new Date().getTime() / 1000 const getSeconds = () => new Date().getTime() / 1000
const bigNumberWords = [ const commas = num => num.toLocaleString()
['tredecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000],
['duodecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000],
['undecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000],
['decillion', 1_000_000_000_000_000_000_000_000_000_000_000],
['nonillion', 1_000_000_000_000_000_000_000_000_000_000],
['octillion', 1_000_000_000_000_000_000_000_000_000],
['septillion', 1_000_000_000_000_000_000_000_000],
['sextillion', 1_000_000_000_000_000_000_000],
['quintillion', 1_000_000_000_000_000_000],
['quadrillion', 1_000_000_000_000_000],
['trillion', 1_000_000_000_000],
['billion', 1_000_000_000],
['million', 1_000_000],
]
const commas = (num, precise = false, skipWords = false) => {
num = Math.round(num)
if (num === 1) {
return 'one'
}
const bigNum = bigNumberWords.find(([, base]) => num >= base)
if (bigNum && !precise) {
const [name, base] = bigNum
const nummed = (num / base).toPrecision(3)
if (skipWords) {
return nummed
}
return `${nummed} ${name}`
}
return num.toLocaleString()
}
const parseAll = (str, allNum, user) => {
if (!str) {
return NaN
}
str = str?.toLowerCase()?.replace(/,/g, '') || '1'
switch (str) {
case 'all':
case 'sugma':
case 'ligma':
case 'pulma':
case 'deez':
case 'max_int':
case 'my soul':
return allNum
case 'sex':
case 'sex number':
return 69_000_000
case ':maple_leaf:':
case ':herb:':
case 'weed':
case 'weed number':
return 420_000_000
case 'a milli':
return 1_000_000
case 'a band':
return 1000
case ':100:':
case 'one hunna':
return 100
}
if (user && buyableItems[str]) {
return calculateCost({ itemName: str, user, quantity: 1 })
}
console.log('STR', str)
if (str.match(/^\d+$/)) {
return parseInt(str)
}
if (allNum && str.match(/^\d+%$/)) {
const percent = parseFloat(str) / 100
if (percent > 1 || percent < 0) {
return NaN
}
return Math.round(percent * allNum)
}
if (str.match(/^\d+\.\d+$/)) {
return Math.round(parseFloat(str))
}
const bigNum = bigNumberWords.find(([name]) => str.endsWith(name))
if (bigNum && str.match(/^\d+(\.\d+)?/)) {
return Math.round(parseFloat(str) * bigNum[1])
}
return NaN
}
const calculateCost = ({ itemName, user, quantity = 1 }) => {
let currentlyOwned = user.items[itemName] || 0
let realCost = 0
for (let i = 0; i < quantity; i++) {
realCost += Math.ceil(buyableItems[itemName].baseCost * Math.pow(1.15, currentlyOwned || 0))
currentlyOwned += 1
}
return realCost
}
const game = loadGame() const game = loadGame()
const { users, nfts, squad } = game const { users, nfts, squad } = game
@ -220,60 +76,50 @@ const addAchievement = (user, achievementName, say) => {
} }
setTimeout(async () => { setTimeout(async () => {
user.achievements[achievementName] = true user.achievements[achievementName] = true
saveGame(`${user.name} earned ${achievementName}`) saveGame()
await say(`You earned the achievement ${achievements[achievementName].name}!`) await say(`You earned the achievement ${achievements[achievementName].name}!`)
}, 500) }, 500)
} }
const fuzzyMatcher = string => new RegExp((string?.toLowerCase() || '').split('').join('.*'), 'i') const getUser = userId => {
if (!users[userId]) {
let knownUsers = {} users[userId] = {
const getIdFromName = name => { coins: 0,
const matcher = fuzzyMatcher(name?.toLowerCase()) items: {},
const found = Object.entries(knownUsers).find(([id, knownName]) => matcher.test(knownName?.toLowerCase())) upgrades: {},
if (found) { achievements: {},
return found[0] coinsAllTime: 0,
prestige: 0
} }
return null; } else {
}
const getUser = (userId, updateCoins = false) => {
users[userId] ??= {}
users[userId].coins ??= 0
users[userId].items ??= {} users[userId].items ??= {}
users[userId].upgrades ??= {} users[userId].upgrades ??= {}
users[userId].achievements ??= {} users[userId].achievements ??= {}
users[userId].coinsAllTime ??= users[userId].coins users[userId].coinsAllTime ??= users[userId].coins
users[userId].prestige ??= 0 users[userId].prestige ??= 0
users[userId].startDate ??= new Date()
if (updateCoins) {
users[userId].coins = getCoins(userId)
} }
return users[userId] return users[userId]
} }
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 getCoins = userId => {
const user = getUser(userId) const user = getUser(userId)
const currentTime = getSeconds() const currentTime = getSeconds()
const lastCheck = user.lastCheck || currentTime const lastCheck = user.lastCheck || currentTime
const secondsPassed = currentTime - lastCheck const secondsPassed = currentTime - lastCheck
addCoins(user, getCPS(user) * secondsPassed) const increase = getCPS(userId) * secondsPassed
user.coins += increase
user.coinsAllTime += increase
user.coins = Math.floor(user.coins)
user.lastCheck = currentTime user.lastCheck = currentTime
setHighestCoins(userId) setHighestCoins(userId)
//saveGame() saveGame()
return user.coins return user.coins
} }
const getCPS = user => { const getCPS = userId => {
const user = getUser(userId)
const userItems = user?.items || {} const userItems = user?.items || {}
return Math.round(Object.keys(userItems).reduce((total, itemName) => total + getItemCps(user, itemName), 0)) return Math.round(Object.keys(userItems).reduce((total, itemName) => total + getItemCps(user, itemName), 0))
} }
@ -284,31 +130,16 @@ const squadUpgrades = {
tastyKeyboards: { tastyKeyboards: {
name: 'Tasty Keyboards', name: 'Tasty Keyboards',
description: 'Delicious and sticky. Boosts CPS by 20% for everyone.', description: 'Delicious and sticky. Boosts CPS by 20% for everyone.',
effect: cps => Math.ceil(cps * 1.2),
effect: cps => cps * 1.2,
cost: 10_000_000_000_000, cost: 10_000_000_000_000,
emoji: 'keyboard' emoji: 'keyboard'
}, },
copyPasteMacro: { copyPasteMacro: {
name: 'Copy-Paste Macro.', name: 'Copy-Paste Macro.',
description: 'Don\'t actually use this. Boosts CPS by 20% for everyone.', description: 'Don\'t actually use this. Boosts CPS by 20% for everyone.',
effect: cps => cps * 1.2, effect: cps => Math.ceil(cps * 1.2),
cost: 100_000_000_000_000, cost: 100_000_000_000_000,
emoji: 'printer' emoji: 'printer'
},
discardHumanMorals: {
name: 'Neglect human decency',
description: `Unlocks a new tier of upgrades, but at what cost?`,
effect: cps => cps * 1.1,
cost: 100_000_000_000_000_000,
emoji: 'hole'
},
redemption: {
name: 'Redemption',
description: 'Can you return from the depths of depravity and save your soul?',
effect: cps => cps * 1.1,
cost: 1_000_000_000_000_000_000,
emoji: 'people_hugging'
} }
} }
@ -320,234 +151,31 @@ const getCompletedSquadgrades = () =>
.filter(squadHas) .filter(squadHas)
.map(([, upgrade]) => upgrade) .map(([, upgrade]) => upgrade)
const getCompletedSquadgradeNames = () =>
Object.entries(squadUpgrades)
.filter(squadHas)
.map(([name]) => name)
const prestigeMultiplier = user => 1 + ((user.prestige || 0) * 0.01)
const quackGradeMultiplier = user => {
const userQuackgrades = user.quackUpgrades?.cps || []
return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1)
}
const petQuackGradeMultiplier = user => {
const userQuackgrades = user.quackUpgrades?.pet || []
return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), petBoost())
}
const singleItemCps = (user, itemName) => { const singleItemCps = (user, itemName) => {
const baseCps = buyableItems[itemName].earning const baseCps = buyableItems[itemName].earning
// console.log('')
// console.log(`${itemName} CPS:`)
// console.log('baseCps', baseCps)
const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name]) const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name])
const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), 1) const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), baseCps)
// console.log('itemUpgradeCps', itemUpgradeCps)
user.upgrades.general ??= [] const userGeneralUpgrades = user.upgrades.general || []
const userGeneralUpgrades = user.upgrades.general const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), itemUpgradeCps)
const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), 1)
// console.log('generalUpgradeCps', generalUpgradeCps)
const achievementCount = Object.keys(user.achievements || {}).length const achievementCount = Object.keys(user.achievements || {}).length
const achievementMultiplier = Math.pow(1.01, achievementCount) const achievementMultiplier = Math.pow(1.01, achievementCount)
// console.log('achievementMultiplier', achievementMultiplier)
const quackGrade = quackGradeMultiplier(user) const userQuackgrades = user.quackUpgrades?.cps || []
// console.log('quackgrade', quackGrade) const quackMultiplier = userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1)
const pMult = prestigeMultiplier(user) const prestigeMultiplier = 1 + ((user.prestige || 0) * 0.01)
// console.log('prestigeMultiplier', pMult)
const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1) return achievementMultiplier *
// console.log('squadGradeMultiplier', squadGradeMultiplier) quackMultiplier *
prestigeMultiplier *
const petMultiplier = petQuackGradeMultiplier(user) getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), generalUpgradeCps)
//console.log('petMultiplier', petMultiplier)
const total =
baseCps *
achievementMultiplier *
itemUpgradeCps *
generalUpgradeCps *
quackGrade *
pMult *
squadGradeMultiplier *
petMultiplier
// console.log('Single Item CPS:', total)
return total
}
const shuffle = str => str.split('').sort(() => 0.5 - Math.random()).join('')
const shufflePercent = (str, percentOdds) => {
const shuffled = shuffle(str)
let partiallyShuffled = ''
const shuffleChar = () => Math.random() < percentOdds
let isEmoji = false
for (let i = 0; i < str.length; i++) {
if (str[i] === ':') {
isEmoji = !isEmoji
}
if (isEmoji) { // Less likely to shuffle emojis
partiallyShuffled += (shuffleChar() && shuffleChar()) ? shuffled[i] : str[i]
} else {
partiallyShuffled += shuffleChar() ? shuffled[i] : str[i]
}
}
return partiallyShuffled
}
const definitelyShuffle = (str, percentOdds) => {
if (!str) {
return str
}
if (!percentOdds) {
percentOdds = 0.01
}
let shuffled = str
while (shuffled === str) {
shuffled = shufflePercent(str, percentOdds)
console.log('Shuffling... "' + shuffled + '"')
}
return shuffled
}
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
/**
* Adds reactions to the given message, in order.
* If adding any reaction is a failure, it will continue on to the next.
*
* @param app The slack bolt app
* @param channelId The id of the channel the message is in
* @param timestamp The timestamp of the message
* @param reactions An array of reactions to add
* @returns {Promise<void>}
*/
const addReactions = async ({ app, channelId, timestamp, reactions }) => {
for (const reaction of reactions) {
try {
await app.client.reactions.add({
channel: channelId,
timestamp,
name: reaction
})
} catch (e) {
logError(e)
}
}
}
const daysSinceEpoch = () => {
const today = new Date().getTime()
const epoch = new Date(0).getTime()
return Math.floor((today - epoch) / (1000 * 60 * 60 * 24))
}
const dayOfYear = () => {
const date = new Date()
return ((Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000)
}
game.stonkMarket ??= {
lastDay: daysSinceEpoch(),
stonks: {
duk: {
pattern: "duk",
index: 0,
price: 1_410_911_983_728
},
quak: {
pattern: "quak",
index: 0,
price: 5_111_242_778_696
},
honk: {
pattern: "honk",
index: 0,
price: 511_915_144_009
},
}
}
const userHasCheckedQuackgrade = (user, quackGrade) => (user.quackUpgrades?.checked || []).includes(quackGrade)
const petBoost = () => {
// game.pet ??= makePet()
const stats = Object.values(game.pet)
const hasTerribleStat = stats.filter(value => value < 1).length > 0
const averageStat = stats.reduce((total, current) => total + current, 0) / stats.length
if (hasTerribleStat && averageStat < 3) {
return 0.9
}
if (averageStat === 10) {
return 1.3
}
if (!hasTerribleStat && averageStat > 8) {
return 1.1
}
return 1
}
game.channelMaps ??= {}
let slackAppClientChatUpdate
/**
*
* @param name String name for this channel map
* @param text String of to send. Passed into slack.app.client.chat.update
* @param blocks Slack blocks object to send. Passed into slack.app.client.chat.update
* @param channel An (optional) new channel to add to the given map
* @param ts The timestamp of the message in the new channel to update
*/
const updateAll = async ({ name, text, blocks, add: { channel, ts } = {} }) => {
const channelMap = (game.channelMaps[name] ??= {})
// if (channel && ts && !channelMap[channel]) {
// }
if (channel && ts) {
channelMap[channel] = ts
console.log({ channelMap })
}
if (text || blocks) {
await Promise.all(Object.entries(channelMap).map(async ([channel, ts]) =>
slackAppClientChatUpdate({
channel,
ts,
text,
blocks
}).catch(e => {
console.error(e)
if (e.toString().includes('message_not_found')) {
delete channelMap[channel]
saveGame(`removing message ${channel}::${ts} from the ${name} list`)
}
})
))
}
// // const alreadyHas = !!channelMap[channel]
// if (channel && ts) {
// channelMap[channel] = ts
// console.log({ channelMap })
// }
// // return alreadyHas
} }
module.exports = { module.exports = {
saveGame, saveGame,
makeBackup,
logError, logError,
parseOr, parseOr,
maybeNews, maybeNews,
@ -562,25 +190,5 @@ module.exports = {
getItemCps, getItemCps,
squadUpgrades, squadUpgrades,
squadIsMissing, squadIsMissing,
prestigeMultiplier, game
quackGradeMultiplier,
shufflePercent,
definitelyShuffle,
parseAll,
getRandomFromArray,
chaosFilter,
addReactions,
getCompletedSquadgradeNames,
game,
dayOfYear,
daysSinceEpoch,
userHasCheckedQuackgrade,
fuzzyMatcher,
addCoins,
calculateCost,
setKnownUsers: users => knownUsers = users,
petBoost,
updateAll,
setSlackAppClientChatUpdate: update => slackAppClientChatUpdate = update,
setUpgrades
} }

View File

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

View File

@ -1,63 +1,43 @@
const slack = require('../slack') const slack = require('../slack')
const { updateAll } = require('../games/hvacoins/utils')
const tie = 'TIE' const tie = 'TIE'
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2, channelMap }) => const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) =>
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2], channelMap) + '\n' + gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' +
'```' + textFromBoard(board) + '\n```' '```' + textFromBoard(board) + '\n```'
const addChoiceEmojis = async ({ choices, channel, ts }) => { const addChoiceEmojis = async ({ choices, channel, ts }) => {
const addEmoji = async emojiName => { const addEmoji = async emojiName =>
try {
await slack.app.client.reactions.add({ await slack.app.client.reactions.add({
channel, channel,
timestamp: ts, timestamp: ts,
name: emojiName name: emojiName
}) })
} catch (ignore) {
}
}
for (const choice of choices) { for (const choice of choices) {
await addEmoji(choice) await addEmoji(choice)
} }
} }
const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => { const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => {
if (event.channel_type !== 'im') { if (event.channel_type === 'im') {
return;
}
const eventText = event.text?.toLowerCase() const eventText = event.text?.toLowerCase()
if (!(eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword)))) { if (eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword))) {
return;
}
try {
console.log('Trigger found')
const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0] const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0]
console.log('Messaging opponent ' + slack.users[opponent.substring(2, opponent.length - 1)]) const msg = messageFromBoard({
const channelMap = {}
const msg = () => messageFromBoard({
dataName, dataName,
gameName, gameName,
textFromBoard, textFromBoard,
board: initialBoard(), board: initialBoard(),
player1: '<@' + event.user + '>', player1: '<@' + event.user + '>',
player2: opponent, player2: opponent
channelMap
}) })
const sent = await say(msg()) const sent = await say(msg)
channelMap[event.user] = { await addChoiceEmojis({ ...sent, choices: turnChoiceEmojis })
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, channelMap = {}) => slack.encodeData(dataKey, { board, players, channelMap }) const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, { board, players })
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message) const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
@ -70,8 +50,7 @@ const getMessages = winner => {
} }
const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turnChoiceEmojis, makeMove }) => async ({ event, say }) => { const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turnChoiceEmojis, makeMove }) => async ({ event, say }) => {
if (event.item_user !== slack.users.Hvacker || !turnChoiceEmojis.includes(event.reaction)) { if (event.item_user !== slack.hvackerBotUserId || !turnChoiceEmojis.includes(event.reaction)) {
console.log('bad item_user/reaction')
return return
} }
@ -82,15 +61,12 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
const game = decodeGame(dataName, message.messages[0].text) const game = decodeGame(dataName, message.messages[0].text)
if (!game) { if (!game) {
console.log('could not decode game')
return return
} }
game.channelMap ??= {} const { board, players } = game
const { board, players, channelMap } = game
let winner = checkWinner(board) let winner = checkWinner(board)
if (winner) { if (winner) {
console.log('winner found: ' + winner)
return return
} }
@ -104,58 +80,32 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
} }
winner = checkWinner(board) winner = checkWinner(board)
const boardMessage = () => messageFromBoard({ const boardMessage = messageFromBoard({
dataName, dataName,
gameName, gameName,
textFromBoard, textFromBoard,
board, board,
player1, player1,
player2, player2
channelMap
}) })
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) const winnerMessages = getMessages(winner)
// await say(boardMessage() + winnerMessages.you) await say(boardMessage + winnerMessages.you)
console.log('TurnHandler', { gameName, boardMessage: boardMessage() }) if (!winner) {
// await updateAll({ name: gameName, text: boardMessage() + '\nTurnHandler' }) await say('Waiting for opponent\'s response...')
// if (!winner) { }
// await say('Waiting for opponent\'s response...')
// }
// const removeEmoji = async emojiName => const removeEmoji = async emojiName =>
// slack.app.client.reactions.remove({ slack.app.client.reactions.remove({
// channel: event.item.channel, channel: event.item.channel,
// timestamp: message.messages[0]?.ts, timestamp: message.messages[0]?.ts,
// name: emojiName name: emojiName
// }) })
// turnChoiceEmojis.forEach(removeEmoji) turnChoiceEmojis.forEach(removeEmoji)
console.log('SENDING to ' + opponent)
if (!channelMap[opponent]) {
const sentBoard = await slack.app.client.chat.postMessage({ const sentBoard = await slack.app.client.chat.postMessage({
channel: opponent, channel: opponent,
text: boardMessage() + winnerMessages.opponent text: boardMessage + winnerMessages.opponent
}) })
channelMap[opponent] = {
channel: sentBoard.channel,
ts: sentBoard.ts
}
console.log('BOARD MESSAGE AFTER ')
await updateAll({ name: gameName, text: boardMessage(), add: sentBoard })
} else {
await updateAll({ name: gameName, text: boardMessage() })
}
if (!winner) { if (!winner) {
const sentBoard = channelMap[opponent]
await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis }) await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis })
} }
} }

View File

@ -1,12 +1,12 @@
const routine = require('./routine') const routine = require("./routine");
const emptyBoard = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '] const emptyBoard = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
const textFromBoard = board => const textFromBoard = board =>
` ${board[0]} | ${board[1]} | ${board[2]} \n` + ` ${board[0]} | ${board[1]} | ${board[2]} \n` +
'-----------\n' + `-----------\n` +
` ${board[3]} | ${board[4]} | ${board[5]} \n` + ` ${board[3]} | ${board[4]} | ${board[5]} \n` +
'-----------\n' + `-----------\n` +
` ${board[6]} | ${board[7]} | ${board[8]}` ` ${board[6]} | ${board[7]} | ${board[8]}`
const numEmojis = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] const numEmojis = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
@ -21,7 +21,7 @@ const winningThrees = [
[2, 5, 8], [2, 5, 8],
[0, 4, 8], [0, 4, 8],
[2, 4, 6] [2, 4, 6],
] ]
const checkWinner = board => { const checkWinner = board => {
@ -71,3 +71,4 @@ routine.build({
makeMove: applyTurn, makeMove: applyTurn,
checkWinner checkWinner
}) })

View File

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

View File

@ -26,12 +26,12 @@ onTempChangeRequested(change => {
case 'Hotter': { case 'Hotter': {
lowTemp += 2 lowTemp += 2
highTemp += 2 highTemp += 2
break break;
} }
case 'Colder': { case 'Colder': {
lowTemp -= 2 lowTemp -= 2
highTemp -= 2 highTemp -= 2
break break;
} }
case 'Good': { case 'Good': {
return return
@ -42,9 +42,9 @@ onTempChangeRequested(change => {
lowTemp = cleanTemp(lowTemp) lowTemp = cleanTemp(lowTemp)
const mode = const mode =
indoorTemperature < lowTemp ? heatMode // Heat if lower than low indoorTemperature < lowTemp ? heatMode : // Heat if lower than low
: indoorTemperature > highTemp ? coolMode // Cool if hotter than high indoorTemperature > highTemp ? coolMode : // Cool if hotter than high
: change === 'Hotter' ? heatMode : coolMode // Otherwise (lower priority) follow the requested change change === 'Hotter' ? heatMode : coolMode // Otherwise (lower priority) follow the requested change
if (!mode) { if (!mode) {
return return

View File

@ -1,22 +1,20 @@
const { App: SlackApp } = require('@slack/bolt') const { App: SlackApp } = require('@slack/bolt')
const config = require('../config') const config = require('../config')
const fs = require('fs')
const { addReactions, saveGame, setSlackAppClientChatUpdate, parseOr } = require('../games/hvacoins/utils')
const temperatureChannelId = 'C034156CE03' const temperatureChannelId = 'C034156CE03'
const dailyStandupChannelId = 'C03L533AU3Z' const hvackerBotUserId = 'U0344TFA7HQ'
const sageUserId = 'U028BMEBWBV'
const pollingMinutes = 5 const pollingMinutes = 5
const pollingPeriod = 1000 * 60 * pollingMinutes const pollingPeriod = 1000 * 60 * pollingMinutes
const MAX_POLLS = 3
const HOURS_PER_WINDOW = 2
const colderEmoji = 'snowflake' const colderEmoji = 'snowflake'
const hotterEmoji = 'fire' const hotterEmoji = 'fire'
const goodEmoji = '+1' const goodEmoji = '+1'
const app = new SlackApp({ let app
try {
app = new SlackApp({
token: config.slackBotToken, token: config.slackBotToken,
signingSecret: config.slackSigningSecret, signingSecret: config.slackSigningSecret,
appToken: config.slackAppToken, appToken: config.slackAppToken,
@ -26,8 +24,11 @@ const app = new SlackApp({
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id // temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
// console.log('techThermostatChannelId', temperatureChannelId) // console.log('techThermostatChannelId', temperatureChannelId)
// }) // })
} catch (e) {
console.log('Failed to initialize SlackApp', e)
}
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater'] const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold']
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>'] const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
const sendHelp = async (say, prefix) => { const sendHelp = async (say, prefix) => {
@ -40,8 +41,7 @@ const sendHelp = async (say, prefix) => {
await say({ await say({
text: prefix + text: prefix +
`Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` + `Sending a message matching any of \`${pollTriggers.join('`, `')}\` will start a temperature poll.\n` +
'\'Hotter\' and \'Colder\' votes offset. E.g. with votes Hotter - 4, Colder - 3, and Content - 2, the temp won\'t change.\n' + '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 ${users.ThermoController}.`
}) })
} }
@ -53,120 +53,83 @@ const getMessage = async ({ channel, ts }) => app.client.conversations.history({
}) })
app.event('reaction_added', async ({ event, context, client, say }) => { app.event('reaction_added', async ({ event, context, client, say }) => {
console.log('reaction_added', event)
for (const listener of reactionListeners) { for (const listener of reactionListeners) {
listener({ event, say }) listener({ event, say })
} }
}) })
const users = parseOr(fs.readFileSync('./users.json', 'utf-8'), const ourUsers = {
() => ({})) U028BMEBWBV: 'Sage',
U02U15RFK4Y: 'Adam',
const buildSayPrepend = ({ say, prepend }) => async msg => { U02AAB54V34: 'Houston',
if (typeof(msg) === 'string') { U02KYLVK1GV: 'Quade',
return say(prepend + msg) U017PG4EL1Y: 'Max',
} UTDLFGZA5: 'Tyler',
return say({ U017CB5L1K3: 'Andres'
...msg,
text: prepend + msg.text
})
} }
process.once('SIGINT', code => {
saveGame('SIGINT', true)
process.exit()
})
let pollHistory = []
const activePolls = {} const activePolls = {}
const testId = 'U028BMEBWBV_TEST' const testId = 'U028BMEBWBV_TEST'
let testMode = false let testMode = false
app.event('message', async ({ event, context, client, say }) => { app.event('message', async ({ event, context, client, say }) => {
if (event.subtype !== 'message_changed' && event?.text !== '!') { console.log(event)
console.log('message.event', { if (event.user === sageUserId) {
...event,
userName: users[event.user]
})
}
if (event?.user === users.Admin) {
if (event?.text.startsWith('!')) { if (event?.text.startsWith('!')) {
if (testMode) { if (testMode) {
await messageAdmin('Currently in test mode!') await messageSage('Currently in test mode!')
} }
} }
if (event?.text === '!test') { if (event?.text === '!test') {
testMode = !testMode testMode = !testMode
await messageAdmin(`TestMode: ${testMode} with ID ${testId}`) await messageSage(`TestMode: ${testMode} with ID ${testId}`)
} else if (event?.text === '!notest') { } else if (event?.text === '!notest') {
testMode = false testMode = false
await messageAdmin(`TestMode: ${testMode}`) await messageSage(`TestMode: ${testMode}`)
} }
if (testMode) { if (testMode) {
event.user = testId event.user = testId
} }
// console.log(event.blocks[0].elements[0])
} }
for (const listener of messageListeners) { for (const listener of messageListeners) {
listener({ event, say }) listener({ event, say })
} }
if (event.user) { console.log('MSG', ourUsers[event.user], "'" + event.text + "'", new Date().toLocaleTimeString())
console.log('MSG', users[event.user], "'" + event.text + "'", new Date().toLocaleTimeString()) if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') {
}
if (event.user === users.Admin && event.channel === 'D0347Q4H9FE') {
if (event.text === '!!kill') { if (event.text === '!!kill') {
saveGame('!!kill', true) process.exit()
process.exit(1)
} else if (event.text === '!!restart') {
if (Object.entries(activePolls).length === 0) {
saveGame('!!restart', true)
process.exit(0)
} else {
await messageAdmin('Restart pending poll completion...')
pendingRestart = true
}
} }
if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) { if (event.text?.startsWith('!say ') || event.text?.startsWith('!say\n')) {
await postToTechThermostatChannel(event.text.substring(4).trim().replace('@here', '<!here>')) await postToTechThermostatChannel(event.text.substring(4).trim())
return
}
if (event.text?.startsWith('!saytoq ')) {
await messageQuade(event.text.substring(7).trim())
return
}
if (event.text?.startsWith('!saytos')) {
await messageSage(event.text.substring(7).trim())
return return
} }
} }
const eventText = event.text?.toLowerCase() || '' const eventText = event.text?.toLowerCase() || ''
if (eventText === '!help') { if (eventText.startsWith('!help')) {
await sendHelp(say) await sendHelp(say)
return return
} }
if (!pollTriggers.includes(eventText) || event.user === users.John) { if (!pollTriggers.includes(eventText)) {
if (halfTriggers.includes(eventText)) { if (halfTriggers.includes(eventText)) {
await sendHelp(say, 'It looks like you might want to change the temperature.') await sendHelp(say, 'It looks like you might want to change the temperature.')
} }
return return
} }
if (event.channel !== temperatureChannelId) {
return say(`Please request polls in the appropriate channel.`)
}
if (activePolls[event.channel]) { if (activePolls[event.channel]) {
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" }) await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
return return
} }
const now = new Date()
const windowStart = new Date()
windowStart.setHours(now.getHours() - HOURS_PER_WINDOW)
const pollsInWindow = pollHistory.filter(pollTime => pollTime > windowStart)
const pollText = MAX_POLLS === 1 ? 'poll' : 'polls'
const hourText = HOURS_PER_WINDOW === 1 ? 'hour' : 'hours'
if (pollsInWindow.length >= MAX_POLLS) {
await postToTechThermostatChannel({ text: `You have exceeded the limit of ${MAX_POLLS} ${pollText} per ${HOURS_PER_WINDOW} ${hourText}!` })
return
}
if (pollHistory.push(now) > MAX_POLLS) {
[, ...pollHistory] = pollHistory
}
activePolls[event.channel] = true activePolls[event.channel] = true
const pollTs = await startPoll() const pollTs = await startPoll()
@ -176,73 +139,36 @@ app.event('message', async ({ event, context, client, say }) => {
timestamp: pollTs, timestamp: pollTs,
full: true full: true
}) })
const reactPosters = {}
reactions.message.reactions.forEach(r => r.users.forEach(user => {
reactPosters[user] ??= []
reactPosters[user].push(r.name)
}))
const reactCounts = {} const reactCounts = {}
Object.entries(reactPosters).forEach(([id, votes]) => { reactions.message.reactions.forEach(reaction => { reactCounts[reaction.name] = reaction.count })
console.log(`VOTES FROM ${id}:`, votes)
votes = votes.filter(v => [goodEmoji, hotterEmoji, colderEmoji].find(emoji => v.startsWith(emoji)))
if (votes.length === 1) {
const name = votes[0].replace(/:.*/g, '')
reactCounts[name] ??= 0
reactCounts[name] += 1
}
})
console.log('REACT COUNTS', JSON.stringify(reactCounts))
const contentVotes = reactCounts[goodEmoji] || 0 const contentVotes = reactCounts[goodEmoji]
let hotterVotes = reactCounts[hotterEmoji] || 0 const hotterVotes = reactCounts[hotterEmoji]
let colderVotes = reactCounts[colderEmoji] || 0 const colderVotes = reactCounts[colderEmoji]
console.log('before contentVotes', contentVotes)
console.log('before colderVotes', colderVotes)
console.log('before hotterVotes', hotterVotes)
if (hotterVotes > colderVotes) { let text = 'The people have spoken, and would like to '
hotterVotes -= colderVotes
colderVotes = 0
} else if (colderVotes > hotterVotes) {
colderVotes -= hotterVotes
hotterVotes = 0
}
console.log('after contentVotes', contentVotes)
console.log('after colderVotes', colderVotes)
console.log('after hotterVotes', hotterVotes)
let text
if (hotterVotes > colderVotes && hotterVotes > contentVotes) { if (hotterVotes > colderVotes && hotterVotes > contentVotes) {
text = `<@${users[users.ThermoController]}> The people have spoken, and would like to `
text += 'raise the temperature, quack.' text += 'raise the temperature, quack.'
requestTempChange('Hotter') requestTempChange('Hotter')
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) { } else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
text = `<@${users[users.ThermoController]}> The people have spoken, and would like to `
text += 'lower the temperature, quack quack.' text += 'lower the temperature, quack quack.'
requestTempChange('Colder') requestTempChange('Colder')
} else { } else {
text = `The people have spoken, and would like to `
text += 'keep the temperature as-is, quaaack.' text += 'keep the temperature as-is, quaaack.'
requestTempChange('Good') requestTempChange('Good')
} }
await postToTechThermostatChannel({ text }) await postToTechThermostatChannel({ text })
delete activePolls[event.channel] delete activePolls[event.channel]
if (pendingRestart && Object.entries(activePolls).length === 0) {
await messageAdmin('Performing pending restart!')
saveGame(null, true)
process.exit(0)
}
}, pollingPeriod) }, pollingPeriod)
}) })
let pendingRestart = false
;(async () => { ;(async () => {
await app.start() await app.start().catch(console.error)
console.log('Slack Bolt has started') console.log('Slack Bolt has started')
// setTimeout(async () => {
// await messageSage('<https://i.imgur.com/VCvfvdz.png|...>')
// }, 2000)
})() })()
const postToTechThermostatChannel = async optionsOrText => { const postToTechThermostatChannel = async optionsOrText => {
@ -254,7 +180,8 @@ const postToTechThermostatChannel = async optionsOrText => {
return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId }) return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId })
} }
const messageAdmin = async optionsOrText => messageIn(users.Admin, optionsOrText) const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText)
const messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText)
const messageIn = async (channel, optionsOrText) => { const messageIn = async (channel, optionsOrText) => {
if (optionsOrText === null || typeof optionsOrText !== 'object') { if (optionsOrText === null || typeof optionsOrText !== 'object') {
@ -267,16 +194,19 @@ const messageIn = async (channel, optionsOrText) => {
const startPoll = async () => { const startPoll = async () => {
const sent = await postToTechThermostatChannel({ const sent = await postToTechThermostatChannel({
text: `<!here> Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted. text: `<!here|here> Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted.\n` +
Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is. `Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is.` +
(Note that I can't actually change the temperature yet. Make ${users.ThermoController} do it!)` `\n(Note that I can't actually change the temperature yet. Make Quade do it!)`
}) })
await addReactions({ const addReaction = async emojiName =>
app, app.client.reactions.add({
channelId: temperatureChannelId, channel: temperatureChannelId,
timestamp: sent.ts, timestamp: sent.ts,
reactions: [colderEmoji, hotterEmoji, goodEmoji] name: emojiName
}) })
await addReaction(colderEmoji)
await addReaction(hotterEmoji)
await addReaction(goodEmoji)
return sent.ts return sent.ts
} }
@ -288,12 +218,10 @@ const requestTempChange = change => {
tempChangeListeners.forEach(listener => listener(change)) tempChangeListeners.forEach(listener => listener(change))
} }
// noinspection HttpUrlsUsage
const encodeData = (key, data) => const encodeData = (key, data) =>
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >` `<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
const decodeData = (key, message) => { const decodeData = (key, message) => {
try {
const regex = new RegExp(`http://${key}ZZZ[^|]*`) const regex = new RegExp(`http://${key}ZZZ[^|]*`)
let match = message.match(regex) let match = message.match(regex)
if (!match) { if (!match) {
@ -301,44 +229,25 @@ const decodeData = (key, message) => {
} }
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8')) return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
} catch (e) {
console.error(e)
return null
}
} }
const onReaction = listener => reactionListeners.push(listener) const onReaction = listener => reactionListeners.push(listener)
const channelIsIm = async channel => (await app.client.conversations.info({ channel }))?.channel?.is_im
const wasMyMessage = async event => {
const text = (await app.client.conversations.history({
channel: event.item.channel,
latest: event.item.ts,
limit: 1,
inclusive: true
})).messages[0].text
const decoded = decodeData('commandPayload', text)
return decoded.event.user === event.user
}
onReaction(async ({ event }) => { onReaction(async ({ event }) => {
console.log({ event }) if (event.user === sageUserId && event.reaction === 'x') {
if (event.reaction === 'x' && (event.user === users.Admin || (await wasMyMessage(event)) || await channelIsIm(event.item.channel))) { console.log(event)
try { try {
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts }) await app.client.chat.delete({channel: event.item.channel, ts: event.item.ts})
} catch (e) { } catch (e) {
console.error(e)
} }
} }
}) })
setSlackAppClientChatUpdate(app.client.chat.update)
module.exports = { module.exports = {
app, app,
hvackerBotUserId,
temperatureChannelId, temperatureChannelId,
dailyStandupChannelId,
onAction: app.action, onAction: app.action,
getMessage, getMessage,
updateMessage: app.client.chat.update, updateMessage: app.client.chat.update,
@ -348,12 +257,10 @@ module.exports = {
onReaction, onReaction,
encodeData, encodeData,
decodeData, decodeData,
messageAdmin, sageUserId,
messageSage,
messageIn, messageIn,
testMode, testMode,
testId, testId,
users, ourUsers
buildSayPrepend,
pollTriggers,
pendingRestart
} }