Compare commits

..

10 Commits

Author SHA1 Message Date
Sage Vaillancourt 969a6b900b !whohas fix 2023-08-03 11:57:27 -04:00
Sage Vaillancourt fdc811d49b Remove !user-list 2023-08-03 11:55:17 -04:00
Sage Vaillancourt 7915e3803c Add a bit of a README 2023-08-03 11:38:06 -04:00
Sage Vaillancourt dcabca0c1a Remove the pebblisp code feature 2023-08-03 11:13:50 -04:00
Sage Vaillancourt be0b49393f Move a bit more config into hvacoins.json and users.json 2023-08-03 10:49:02 -04:00
Sage Vaillancourt 4433c19d04 Add Governments.
New betting achievement.
Add !pet
Move from Sage to Admin naming.
Add cursed pics for hauntings (and reduce haunting odds)
Add saveGame() reason messages.
Add mining upgrades.
Add semi-live updating leaderboards.
Add thorough emoji validation.
Move user IDs to a separate json file.
More details (like current coin count) in !buy menu.
Limit temperature polls to prevent spam.
Some work on a prestige menu.
Several additional quackgrades.
Change text games to try editing messages live.
Named upgrades.
2023-06-29 10:41:55 -04:00
Sage Vaillancourt ad021cf9a5 Many additions:
Add several new achievements
Add price querying with ?b
Simplify commands' argument-handling
Some spooky stuff
New quackgrades
Stormy weather
Rebalanced some rare-event odds
!u buttons
Reworked prestige emojis
Save on every command
Add stonks
More temp poll triggers
Redemption upgrades more powerful
Fuzzy matching for usernames and buyables
2022-05-19 11:09:16 -04:00
Sage Vaillancourt 6dabe9d85a Several fixes and additions:
Add Nik.
Consider removing Tyler but don't.
Fix skin-color temp votes.
Fix prestige gating.
Tweak !mine odds.
Pass squadgrades to upgrade condition-checking.
Fix amount parsing in edge cases.
Add changelog.
New quackgrade.
Upgrades re-org.
Add evil/heavenly upgrades dependent on squadgrades.
Add minimum to chaosFilter.
2022-05-03 09:56:10 -04:00
Sage Vaillancourt 4e15721803 New additions
Temp-change votes cancel each other out.
More achievements.
Better !buy layout.
2 new buyableItems.
More advanced command flags.
Centralize user and coin fetching for commands.
Display hvacker's owned soul count.
Rank leaderboard by prestige, then CPS.
Start centralize some settings.

Add:
- Quack Store
- Lore
- Horror mode.
- hidden payloads to messages.
- Lightning strikes.
- Cups game.
- Emojis in non-dm !a calls.
- Human-readable numbers.
- Gamble-loss mockery.
- Chaos.
- Prestige emojis.
2022-04-20 12:05:02 -04:00
Sage Vaillancourt 8577f6f272 More precise ngift error messages. 2022-03-15 11:45:30 -04:00
22 changed files with 9610 additions and 726 deletions

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# 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,8 +6,9 @@ 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, {method:'GET', fetch(url, {
headers: headers, method: 'GET',
//credentials: 'user:passwd' headers: headers
// credentials: 'user:passwd'
}).then(response => response.json()) }).then(response => response.json())
.then(json => console.log(json)); .then(json => console.log('json', json))

View File

@ -5,17 +5,17 @@ const fs = require('fs')
const fileName = 'hvackerconfig.json' const fileName = 'hvackerconfig.json'
const configPaths = [ const configPaths = [
path.join('/secrets', fileName), path.join('/secrets', fileName),
path.join(homedir, '.add123', fileName) path.join(homedir, '.add123', fileName)
] ]
const getConfigData = () => { const getConfigData = () => {
const configPath = configPaths.find(fs.existsSync) const configPath = configPaths.find(fs.existsSync)
if (!configPath) { if (!configPath) {
throw 'Could not find a config file!' throw 'Could not find a config file!'
} }
const config = fs.readFileSync(configPath, 'utf8') const config = fs.readFileSync(configPath, 'utf8')
return JSON.parse(config) return JSON.parse(config)
} }
module.exports = getConfigData() module.exports = getConfigData()

View File

@ -1,12 +1,12 @@
const routine = require('./routine') const routine = require('./routine')
const emptyBoard = [ const emptyBoard = [
[' ', ' ', ' ', ' ', ' ', ' ', ' ',], [' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ',], [' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ',], [' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ',], [' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ',], [' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ',] [' ', ' ', ' ', ' ', ' ', ' ', ' ']
] ]
const textFromBoard = board => { const textFromBoard = board => {
@ -30,7 +30,7 @@ const checkRows = board => {
const checkColumns = board => { const checkColumns = board => {
const colToText = i => const colToText = i =>
board[0][i] + board[0][i] +
board[1][i] + board[1][i] +
board[2][i] + board[2][i] +
board[3][i] + board[3][i] +
@ -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
} }
} }
@ -110,7 +110,7 @@ const placeAt = (i, board, char) => {
} }
const makeMove = (emoji, board) => const makeMove = (emoji, board) =>
placeAt(numEmojis.indexOf(emoji), board, getTurn(board)) placeAt(numEmojis.indexOf(emoji), board, getTurn(board))
routine.build({ routine.build({
startTriggers: ['connect 4', 'c4'], startTriggers: ['connect 4', 'c4'],
@ -120,4 +120,4 @@ routine.build({
textFromBoard, textFromBoard,
checkWinner: board => checkRows(board) || checkColumns(board) || checkDiagonals(board) || checkFull(board), checkWinner: board => checkRows(board) || checkColumns(board) || checkDiagonals(board) || checkFull(board),
makeMove makeMove
}) })

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,6 +34,21 @@ 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!',
@ -45,8 +60,79 @@ 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'
}, },
@ -71,9 +157,25 @@ 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,25 +1,17 @@
const buyableItems = require('./buyableItems'); const buyableItems = require('./buyableItems')
const { commas, saveGame, setHighestCoins, addAchievement, getCoins, getUser, singleItemCps } = require('./utils'); const { commas, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter, fuzzyMatcher, calculateCost } = require('./utils')
const slack = require('../../slack') const slack = require('../../slack')
const calculateCost = ({ itemName, user, quantity = 1 }) => { const leaderboardUpdater = {}
let currentlyOwned = user.items[itemName] || 0
let realCost = 0
for (let i = 0; i < quantity; i++) {
realCost += Math.ceil(buyableItems[itemName].baseCost * Math.pow(1.15, currentlyOwned || 0))
currentlyOwned += 1
}
return realCost
}
const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => { const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => {
const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost) const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost)
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 = highestCoins => ([, item]) => item.baseCost < (highestCoins || 1) * 101 const canView = (item, highestCoins) => item.baseCost < (highestCoins || 1) * 101
const buyableText = (highestCoins, user) => Object.entries(buyableItems) const buyableText = (highestCoins, user) => Object.entries(buyableItems)
.filter(canView(highestCoins)) .filter(([, item]) => canView(item, 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:' +
@ -29,84 +21,138 @@ 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} - 𝕳${commas(cost)} - ${commas(cps)} CPS\n_${buyableItems[itemName].description}_` text: `${itemName} :${buyableItems[itemName].emoji}:x${user.items[itemName] || 0} - H${commas(cost)} - ${commas(cps)} CPS\n_${buyableItems[itemName].description}_`
}, },
accessory: { accessory: {
type: 'button', type: 'button',
text: { text: {
type: 'plain_text', type: 'plain_text',
text: 'Buy 1', text: '1',
emoji: true emoji: true
}, },
value: 'buy_' + itemName, value: 'buy_' + itemName,
action_id: 'buy_' + itemName action_id: 'buy_' + itemName
}, }
}) })
const buyText2 = (highestCoins, user) => { const buildBlock2 = ({ user, itemName, cost, cps }) => ({
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'Buy 1',
emoji: true
},
value: '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 = '') => {
return ({ return ({
text: buyableText(highestCoins, user), text: (extraMessage && extraMessage + '\n')
blocks: Object.entries(buyableItems) + `You have ${commas(user.coins)} HVAC to spend.\n`
.filter(canView(highestCoins)) + buyableText(highestCoins, user),
.map(([itemName, item]) => { blocks: [
const cost = calculateCost({ itemName, user, quantity: 1 }) {
const cps = Math.round(singleItemCps(user, itemName)) type: 'section',
return ({ user, itemName, cost, cps }) text: {
}).map(buildBlock) type: 'mrkdwn',
text: (extraMessage && extraMessage + '\n')
+ `You have ${commas(user.coins)} HVAC to spend.\n`
},
},
...Object.entries(buyableItems)
.filter(([, item]) => canView(item, highestCoins))
.map(([itemName]) => {
const cost = calculateCost({ itemName, user, quantity: 1 })
const cps = Math.round(singleItemCps(user, itemName))
return ({ user, itemName, cost, cps })
}).map(buildBlock)
]
}) })
} }
const buyRoute = async ({ event, say, words }) => { const maxQuantity = ({ itemName, user, currentCoins }) => {
const user = getUser(event.user) let quantity = 1
const buying = words[1] while (calculateCost({ itemName, user, quantity: quantity + 1 }) <= currentCoins) {
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 (buyableItems.quade.baseCost < highestCoins * 100) { if (canView(buyableItems.quade, highestCoins)) {
addAchievement(user, 'seeTheQuade', say) addAchievement(user, 'seeTheQuade', say)
} }
await say(buyText2(highestCoins, user)) await say(buyText2(highestCoins, user))
return return
} }
const buyable = buyableItems[buying] const matcher = fuzzyMatcher(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
} }
let quantity = 1 const [buyableName, buyableItem] = buyable
const currentCoins = getCoins(event.user) let quantity
if (words[2] === 'max') { const currentCoins = user.coins
while (calculateCost({ itemName: buying, user, quantity: quantity + 1 }) <= currentCoins) { const max = maxQuantity({ itemName: buyableName, user, currentCoins })
quantity++ if (!args[1]) {
} quantity = 1
} else if (args[1] === 'max') {
quantity = max
} else { } else {
quantity = parseInt(words[2] || '1') if (query) {
quantity = parseInt(args[1])
} else {
quantity = Math.round(chaosFilter(parseInt(args[1]), 0.2, user, max) || 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: buying, user, quantity }) const realCost = calculateCost({ itemName: buyableName, user, quantity })
if (query) {
return say(`Buying ${quantity} ${buyableName} would cost you ${commas(realCost)} HVAC`)
}
if (currentCoins < realCost) { if (currentCoins < realCost) {
await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`) await say(`You don't have enough coins! You need ${commas(realCost)}`)
return return
} }
user.coins -= realCost user.coins -= realCost
user.items[buying] = user.items[buying] || 0 user.items[buyableName] = user.items[buyableName] || 0
user.items[buying] += quantity user.items[buyableName] += quantity
console.log(buying, user.items.mouse)
if (buying === 'mouse' && user.items.mouse >= 100) { if (user.items[buyableName] >= 100) {
addAchievement(user, 'ratGod', say) addAchievement(user, buyableItems[buyableName].own100Achievement, say)
} }
if (quantity === 1) {
await say(`You bought one :${buyable.emoji}:`) const countString = quantity === 1 ? 'one' : quantity
} else { await say(`You bought ${countString} :${buyableItem.emoji}:`)
await say(`You bought ${quantity} :${buyable.emoji}:`)
}
saveGame()
} }
const buyButton = async ({ body, ack, say, payload }) => { const buyButton = async ({ body, ack, say, payload }) => {
@ -114,19 +160,25 @@ 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, '1'] const words = ['', buying, body.actions[0].text]
await buyRoute({ event, say, words }) const [commandName, ...args] = words
let extraMessage = ''
say = async text => extraMessage = text
await buyRoute({ event, say, words, args, commandName, user })
const highestCoins = user.highestEver || user.coins || 1 const highestCoins = user.highestEver || user.coins || 1
await slack.app.client.chat.update({ await slack.app.client.chat.update({
channel: body.channel.id, channel: body.channel.id,
ts: body.message.ts, ts: body.message.ts,
...buyText2(highestCoins, user) ...buyText2(highestCoins, user, extraMessage)
}) })
await leaderboardUpdater.updateAllLeaderboards()
} }
Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton)) Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton))
module.exports = buyRoute module.exports = { buyRoute, leaderboardUpdater }

View File

@ -3,72 +3,105 @@ 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

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

File diff suppressed because it is too large Load Diff

113
src/games/hvacoins/lore.js Normal file
View File

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

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

View File

@ -1,227 +1,666 @@
const basic = ({ type, description, count, cost }) => ({ const { getCPS, setUpgrades } = require('./utils');
const basic = ({ name, type, description, count, cost, extraCondition = () => true, effect = cps => cps * 2 }) => ({
name,
type, type,
description, description,
condition: user => user.items[type] >= count, condition: (user, squadGrades) => user.items[type] >= count && extraCondition(user, squadGrades),
cost, cost,
effect: itemCps => itemCps * 2 effect
})
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: 'Use the ignite command for a secret achievement.', description: 'Hey, psst, hey. Use the ignite command for a secret achievement.',
count: 10, count: 10,
cost: 163_000_000, cost: 163_000_000
}), }),
cavemanFire: basic({ cavemanFire: basic({
name: 'Caveman Fire',
type: 'fire', type: 'fire',
description: 'They just don\'t make \'em like they used to.', description: 'They just don\'t make \'em like they used to.',
count: 25, count: 25,
cost: 700_000_000, cost: 700_000_000
}),
lava: evil({
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) => Math.ceil(itemCps * 1.1) effect: (itemCps, user) => itemCps * 1.1
}, },
iLoveHvac: { iLoveHvac: {
name: 'iLoveHvac',
type: 'general', type: 'general',
description: 'The power of love increases your overall CPS by 10%', description: 'The power of love increases your overall CPS by 10%',
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400, condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
emoji: 'heart', emoji: 'heart',
cost: 100_000_000_000_000, cost: 100_000_000_000_000,
effect: (itemCps, user) => Math.ceil(itemCps * 1.1) effect: (itemCps, user) => itemCps * 1.1
} },
// moreUpgrades: { digitalPickaxe: {
// type: 'general', name: 'Digital Pickaxe',
// description: 'Adds additional upgrades', type: 'mining',
// condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400, description: 'Break coinful digirocks into bits, increasing the power of !mine',
// emoji: 'cookie', condition: user => user.interactions > 100,
// cost: 10_000_000_000_000, emoji: 'pick',
// effect: nothing cost: 100_000,
// }, effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
},
vacuum: {
name: 'Digital Vacuum',
type: 'mining',
description: 'Suck up leftover HVAC dust, greatly increasing the power of !mine',
condition: user => user.interactions > 500,
emoji: 'vacuum',
cost: 10_000_000,
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
},
mineCart: {
name: 'HVAC Mine Cart',
type: 'mining',
description: 'You\'d shine like a diamond, down in the !mine',
condition: user => user.interactions > 1500,
emoji: 'shopping_trolley',
cost: 100_000_000_000,
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
},
fpga: {
name: 'FPGA Miner',
type: 'mining',
description: 'Wait, what kind of mining is this again?',
condition: user => user.interactions > 5000,
emoji: 'floppy_disk',
cost: 1_000_000_000_000_000,
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
}
} }
setUpgrades(module.exports)

View File

@ -1,37 +1,79 @@
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 upgrades = require("./upgrades"); const { quackStore, getChaos } = require('./quackstore')
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(msg) : () => { /* Don't log empty message */ } const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ }
const loadGame = () => parseOr(fs.readFileSync('./' + saveFile, 'utf-8'), const loadGame = () => {
() => ({ const game = parseOr(fs.readFileSync('./' + saveFile, 'utf-8'),
users: {}, () => ({
nfts: [], users: {},
squad: {} nfts: [],
})) squad: {},
horrors: {}
}))
game.horrors ??= {}
return game
}
const parseOr = (parseable, orFunc) => { const chaosFilter = (num, odds, user, max = Infinity, min = -Infinity) => {
const userQuackgrades = user.quackUpgrades?.cps || []
const hasChaos = userQuackgrades.includes('chaos')
if (!hasChaos || Math.random() < odds || !num) {
return num
}
const chaosed = num * getChaos(user)
if (chaosed > max) {
return max
}
if (chaosed < min) {
return min
}
return chaosed
}
const parseOr = (parseable, fallback) => {
try { try {
if (typeof parseable === 'function') {
parseable = parseable()
}
return JSON.parse(parseable) return JSON.parse(parseable)
} catch (e) { } catch (e) {
logError(e) logError(e)
return orFunc() return fallback()
} }
} }
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 = () => { const saveGame = (after, force = true) => {
if (saves % 100 === 0) { if (saves % 20 === 0) {
fs.writeFileSync('./backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_'), JSON.stringify(game)) makeBackup()
} }
saves += 1 saves += 1
fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2)) 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))
}
} }
const maybeNews = say => { const maybeNews = say => {
@ -47,14 +89,116 @@ const maybeNews = say => {
const idFromWord = word => { const idFromWord = word => {
if (!word?.startsWith('<@') || !word.endsWith('>')) { if (!word?.startsWith('<@') || !word.endsWith('>')) {
return null return getIdFromName(word)
} 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 commas = num => num.toLocaleString() const bigNumberWords = [
['tredecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000],
['duodecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000],
['undecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000],
['decillion', 1_000_000_000_000_000_000_000_000_000_000_000],
['nonillion', 1_000_000_000_000_000_000_000_000_000_000],
['octillion', 1_000_000_000_000_000_000_000_000_000],
['septillion', 1_000_000_000_000_000_000_000_000],
['sextillion', 1_000_000_000_000_000_000_000],
['quintillion', 1_000_000_000_000_000_000],
['quadrillion', 1_000_000_000_000_000],
['trillion', 1_000_000_000_000],
['billion', 1_000_000_000],
['million', 1_000_000],
]
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
@ -76,50 +220,60 @@ const addAchievement = (user, achievementName, say) => {
} }
setTimeout(async () => { setTimeout(async () => {
user.achievements[achievementName] = true user.achievements[achievementName] = true
saveGame() saveGame(`${user.name} earned ${achievementName}`)
await say(`You earned the achievement ${achievements[achievementName].name}!`) await say(`You earned the achievement ${achievements[achievementName].name}!`)
}, 500) }, 500)
} }
const getUser = userId => { const fuzzyMatcher = string => new RegExp((string?.toLowerCase() || '').split('').join('.*'), 'i')
if (!users[userId]) {
users[userId] = { let knownUsers = {}
coins: 0, const getIdFromName = name => {
items: {}, const matcher = fuzzyMatcher(name?.toLowerCase())
upgrades: {}, const found = Object.entries(knownUsers).find(([id, knownName]) => matcher.test(knownName?.toLowerCase()))
achievements: {}, if (found) {
coinsAllTime: 0, return found[0]
prestige: 0 }
} return null;
} else { }
users[userId].items ??= {}
users[userId].upgrades ??= {} const getUser = (userId, updateCoins = false) => {
users[userId].achievements ??= {} users[userId] ??= {}
users[userId].coinsAllTime ??= users[userId].coins users[userId].coins ??= 0
users[userId].prestige ??= 0 users[userId].items ??= {}
users[userId].upgrades ??= {}
users[userId].achievements ??= {}
users[userId].coinsAllTime ??= users[userId].coins
users[userId].prestige ??= 0
users[userId].startDate ??= new Date()
if (updateCoins) {
users[userId].coins = getCoins(userId)
} }
return users[userId] 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
const increase = getCPS(userId) * secondsPassed addCoins(user, getCPS(user) * 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 = userId => { const getCPS = user => {
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))
} }
@ -130,16 +284,31 @@ 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 => Math.ceil(cps * 1.2), effect: cps => 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'
} }
} }
@ -147,35 +316,238 @@ const squadHas = ([name]) => squad.upgrades[name] === true
const squadIsMissing = name => !squadHas(name) const squadIsMissing = name => !squadHas(name)
const getCompletedSquadgrades = () => const getCompletedSquadgrades = () =>
Object.entries(squadUpgrades) Object.entries(squadUpgrades)
.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), baseCps) const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), 1)
// console.log('itemUpgradeCps', itemUpgradeCps)
const userGeneralUpgrades = user.upgrades.general || [] user.upgrades.general ??= []
const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), itemUpgradeCps) const userGeneralUpgrades = user.upgrades.general
const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), 1)
// console.log('generalUpgradeCps', generalUpgradeCps)
const achievementCount = Object.keys(user.achievements || {}).length const 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 userQuackgrades = user.quackUpgrades?.cps || [] const quackGrade = quackGradeMultiplier(user)
const quackMultiplier = userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1) // console.log('quackgrade', quackGrade)
const prestigeMultiplier = 1 + ((user.prestige || 0) * 0.01) const pMult = prestigeMultiplier(user)
// console.log('prestigeMultiplier', pMult)
return achievementMultiplier * const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1)
quackMultiplier * // console.log('squadGradeMultiplier', squadGradeMultiplier)
prestigeMultiplier *
getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), generalUpgradeCps) const petMultiplier = petQuackGradeMultiplier(user)
//console.log('petMultiplier', petMultiplier)
const total =
baseCps *
achievementMultiplier *
itemUpgradeCps *
generalUpgradeCps *
quackGrade *
pMult *
squadGradeMultiplier *
petMultiplier
// console.log('Single Item CPS:', total)
return total
}
const shuffle = str => str.split('').sort(() => 0.5 - Math.random()).join('')
const shufflePercent = (str, percentOdds) => {
const shuffled = shuffle(str)
let partiallyShuffled = ''
const shuffleChar = () => Math.random() < percentOdds
let isEmoji = false
for (let i = 0; i < str.length; i++) {
if (str[i] === ':') {
isEmoji = !isEmoji
}
if (isEmoji) { // Less likely to shuffle emojis
partiallyShuffled += (shuffleChar() && shuffleChar()) ? shuffled[i] : str[i]
} else {
partiallyShuffled += shuffleChar() ? shuffled[i] : str[i]
}
}
return partiallyShuffled
}
const definitelyShuffle = (str, percentOdds) => {
if (!str) {
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,
@ -190,5 +562,25 @@ module.exports = {
getItemCps, getItemCps,
squadUpgrades, squadUpgrades,
squadIsMissing, squadIsMissing,
game prestigeMultiplier,
} 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 } } = require('./utils') const { game: { users }, getUser, fuzzyMatcher } = require('./utils')
const apiGetUserId = hash => { const apiGetUserId = hash => {
return Object.entries(userGetter.users) return Object.entries(users)
.filter(([id, user]) => user.pwHash === hash) .filter(([id, user]) => user.pwHash === hash)
.map(([id, user]) => id)[0] .map(([id, user]) => id)[0]
} }
@ -17,12 +17,21 @@ 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) const say = async msg => res.send(msg + '\n')
try { try {
const words = ['', ...Object.keys(req.query)] const words = ['', ...Object.keys(req.query)]
console.log('INCOMING API CALL:', name, words) const [commandName, ...args] = words
console.log('INCOMING API CALL:', commandName, words)
const encoded = req.header('Authorization').substring(5) const encoded = req.header('Authorization').substring(5)
const decoded = base64.decode(encoded).substring(1) const decoded = base64.decode(encoded).substring(1)
const event = { const event = {
@ -37,8 +46,8 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
console.log(' bad password') console.log(' bad password')
return return
} }
const lastCall = userGetter.users[event.user].lastApiCall || 0 const lastCall = lastCalls[event.user] || 0
const secondsBetweenCalls = 5 const secondsBetweenCalls = 30
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)
@ -46,13 +55,22 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
console.log(' rate limited') console.log(' rate limited')
return return
} }
console.log(` went through for ${slack.ourUsers[event.user]}`) console.log(` went through for ${slack.users[event.user]}`)
userGetter.users[event.user].lastApiCall = currentTime lastCalls[event.user] = currentTime
await action({event, say, words}) const user = getUser(event.user)
const haunted = false
//await action({ event, say, words, args, commandName })
const canUse = await condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.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(e) console.error('route error', e)
await say(e.stack) await say(`Routing error. Make sure you've set up API access with the !setpw command in slack!\n` +
'Then you can use calls like `curl -u ":yourpw" \'http://10.3.0.48:3001/stonks\'`')
} }
} }
commandNames.forEach(name => commandNames.forEach(name =>
@ -66,4 +84,4 @@ module.exports = {
launch: () => app.listen(port, () => { launch: () => app.listen(port, () => {
console.log(`Express listening on port ${port}`) console.log(`Express listening on port ${port}`)
}) })
} }

View File

@ -1,43 +1,63 @@
const slack = require('../slack') const slack = require('../slack')
const { updateAll } = require('../games/hvacoins/utils')
const tie = 'TIE' const tie = 'TIE'
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) => const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2, channelMap }) =>
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' + gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2], channelMap) + '\n' +
'```' + textFromBoard(board) + '\n```' '```' + textFromBoard(board) + '\n```'
const addChoiceEmojis = async ({ choices, channel, ts }) => { const addChoiceEmojis = async ({ choices, channel, ts }) => {
const addEmoji = async emojiName => const addEmoji = async emojiName => {
await slack.app.client.reactions.add({ try {
channel, await slack.app.client.reactions.add({
timestamp: ts, channel,
name: emojiName timestamp: ts,
}) 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') {
const eventText = event.text?.toLowerCase() return;
if (eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword))) { }
const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0] const eventText = event.text?.toLowerCase()
const msg = messageFromBoard({ if (!(eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword)))) {
dataName, return;
gameName, }
textFromBoard, try {
board: initialBoard(), console.log('Trigger found')
player1: '<@' + event.user + '>', const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0]
player2: opponent console.log('Messaging opponent ' + slack.users[opponent.substring(2, opponent.length - 1)])
}) const channelMap = {}
const sent = await say(msg) const msg = () => messageFromBoard({
await addChoiceEmojis({ ...sent, choices: turnChoiceEmojis }) dataName,
gameName,
textFromBoard,
board: initialBoard(),
player1: '<@' + event.user + '>',
player2: opponent,
channelMap
})
const sent = await say(msg())
channelMap[event.user] = {
channel: sent.channel,
ts: sent.ts
} }
await updateAll({ name: gameName, text: msg(), add: sent })
await addChoiceEmojis({...sent, choices: turnChoiceEmojis})
} catch (e) {
console.error(e)
} }
} }
const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, { board, players }) const encodeGame = (dataKey, board, players, channelMap = {}) => slack.encodeData(dataKey, { board, players, channelMap })
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message) const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
@ -50,7 +70,8 @@ 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.hvackerBotUserId || !turnChoiceEmojis.includes(event.reaction)) { if (event.item_user !== slack.users.Hvacker || !turnChoiceEmojis.includes(event.reaction)) {
console.log('bad item_user/reaction')
return return
} }
@ -61,12 +82,15 @@ 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
} }
const { board, players } = game game.channelMap ??= {}
const { board, players, channelMap } = game
let winner = checkWinner(board) let winner = checkWinner(board)
if (winner) { if (winner) {
console.log('winner found: ' + winner)
return return
} }
@ -80,32 +104,58 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
} }
winner = checkWinner(board) winner = checkWinner(board)
const boardMessage = messageFromBoard({ const boardMessage = () => messageFromBoard({
dataName, dataName,
gameName, gameName,
textFromBoard, textFromBoard,
board, board,
player1, player1,
player2 player2,
channelMap
}) })
const winnerMessages = getMessages(winner) if (winner) {
await say(boardMessage + winnerMessages.you) await updateAll({ name: gameName, text: boardMessage() + '\nSomebody won! I do not yet know who!' })
if (!winner) { const removeEmoji = emojiName => Object.values(channelMap).forEach(({ channel, ts }) =>
await say('Waiting for opponent\'s response...') slack.app.client.reactions.remove({
channel,
timestamp: ts,
name: emojiName
}))
turnChoiceEmojis.forEach(removeEmoji)
return
} }
const winnerMessages = getMessages(winner)
// await say(boardMessage() + winnerMessages.you)
console.log('TurnHandler', { gameName, boardMessage: boardMessage() })
// await updateAll({ name: gameName, text: boardMessage() + '\nTurnHandler' })
// if (!winner) {
// await say('Waiting for opponent\'s response...')
// }
const removeEmoji = async emojiName => // 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)
console.log('SENDING to ' + opponent)
if (!channelMap[opponent]) {
const sentBoard = await slack.app.client.chat.postMessage({
channel: opponent,
text: boardMessage() + winnerMessages.opponent
}) })
turnChoiceEmojis.forEach(removeEmoji) channelMap[opponent] = {
const sentBoard = await slack.app.client.chat.postMessage({ channel: sentBoard.channel,
channel: opponent, ts: sentBoard.ts
text: boardMessage + winnerMessages.opponent }
}) 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,4 +71,3 @@ 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(console.error) .catch(e => console.error('trivia error', e))
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,20 +1,22 @@
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 hvackerBotUserId = 'U0344TFA7HQ' const dailyStandupChannelId = 'C03L533AU3Z'
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'
let app const app = new SlackApp({
try {
app = new SlackApp({
token: config.slackBotToken, token: config.slackBotToken,
signingSecret: config.slackSigningSecret, signingSecret: config.slackSigningSecret,
appToken: config.slackAppToken, appToken: config.slackAppToken,
@ -24,11 +26,8 @@ try {
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id // temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
// console.log('techThermostatChannelId', temperatureChannelId) // console.log('techThermostatChannelId', temperatureChannelId)
// }) // })
} catch (e) {
console.log('Failed to initialize SlackApp', e)
}
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold'] const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater']
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>'] const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
const sendHelp = async (say, prefix) => { const sendHelp = async (say, prefix) => {
@ -41,7 +40,8 @@ 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` +
'At this time I am not capable of actually changing the temperature. Go bug Quade.' '\'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 ${users.ThermoController}.`
}) })
} }
@ -53,83 +53,120 @@ 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 ourUsers = { const users = parseOr(fs.readFileSync('./users.json', 'utf-8'),
U028BMEBWBV: 'Sage', () => ({}))
U02U15RFK4Y: 'Adam',
U02AAB54V34: 'Houston', const buildSayPrepend = ({ say, prepend }) => async msg => {
U02KYLVK1GV: 'Quade', if (typeof(msg) === 'string') {
U017PG4EL1Y: 'Max', return say(prepend + msg)
UTDLFGZA5: 'Tyler', }
U017CB5L1K3: 'Andres' return say({
...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 }) => {
console.log(event) if (event.subtype !== 'message_changed' && event?.text !== '!') {
if (event.user === sageUserId) { console.log('message.event', {
...event,
userName: users[event.user]
})
}
if (event?.user === users.Admin) {
if (event?.text.startsWith('!')) { if (event?.text.startsWith('!')) {
if (testMode) { if (testMode) {
await messageSage('Currently in test mode!') await messageAdmin('Currently in test mode!')
} }
} }
if (event?.text === '!test') { if (event?.text === '!test') {
testMode = !testMode testMode = !testMode
await messageSage(`TestMode: ${testMode} with ID ${testId}`) await messageAdmin(`TestMode: ${testMode} with ID ${testId}`)
} else if (event?.text === '!notest') { } else if (event?.text === '!notest') {
testMode = false testMode = false
await messageSage(`TestMode: ${testMode}`) await messageAdmin(`TestMode: ${testMode}`)
} }
if (testMode) { if (testMode) {
event.user = testId event.user = testId
} }
// console.log(event.blocks[0].elements[0])
} }
for (const listener of messageListeners) { for (const listener of messageListeners) {
listener({ event, say }) listener({ event, say })
} }
console.log('MSG', ourUsers[event.user], "'" + event.text + "'", new Date().toLocaleTimeString()) if (event.user) {
if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') { console.log('MSG', users[event.user], "'" + event.text + "'", new Date().toLocaleTimeString())
}
if (event.user === users.Admin && event.channel === 'D0347Q4H9FE') {
if (event.text === '!!kill') { if (event.text === '!!kill') {
process.exit() saveGame('!!kill', true)
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()) await postToTechThermostatChannel(event.text.substring(4).trim().replace('@here', '<!here>'))
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.startsWith('!help')) { if (eventText === '!help') {
await sendHelp(say) await sendHelp(say)
return return
} }
if (!pollTriggers.includes(eventText)) { if (!pollTriggers.includes(eventText) || event.user === users.John) {
if (halfTriggers.includes(eventText)) { if (halfTriggers.includes(eventText)) {
await sendHelp(say, 'It looks like you might want to change the temperature.') await sendHelp(say, 'It looks like you might want to change the temperature.')
} }
return return
} }
if (event.channel !== temperatureChannelId) {
return say(`Please request polls in the appropriate channel.`)
}
if (activePolls[event.channel]) { if (activePolls[event.channel]) {
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" }) await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
return return
} }
const now = new Date()
const windowStart = new Date()
windowStart.setHours(now.getHours() - HOURS_PER_WINDOW)
const pollsInWindow = pollHistory.filter(pollTime => pollTime > windowStart)
const pollText = MAX_POLLS === 1 ? 'poll' : 'polls'
const hourText = HOURS_PER_WINDOW === 1 ? 'hour' : 'hours'
if (pollsInWindow.length >= MAX_POLLS) {
await postToTechThermostatChannel({ text: `You have exceeded the limit of ${MAX_POLLS} ${pollText} per ${HOURS_PER_WINDOW} ${hourText}!` })
return
}
if (pollHistory.push(now) > MAX_POLLS) {
[, ...pollHistory] = pollHistory
}
activePolls[event.channel] = true activePolls[event.channel] = true
const pollTs = await startPoll() const pollTs = await startPoll()
@ -139,36 +176,73 @@ 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 = {}
reactions.message.reactions.forEach(reaction => { reactCounts[reaction.name] = reaction.count }) Object.entries(reactPosters).forEach(([id, votes]) => {
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] const contentVotes = reactCounts[goodEmoji] || 0
const hotterVotes = reactCounts[hotterEmoji] let hotterVotes = reactCounts[hotterEmoji] || 0
const colderVotes = reactCounts[colderEmoji] let colderVotes = reactCounts[colderEmoji] || 0
console.log('before contentVotes', contentVotes)
console.log('before colderVotes', colderVotes)
console.log('before hotterVotes', hotterVotes)
let text = 'The people have spoken, and would like to ' if (hotterVotes > colderVotes) {
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().catch(console.error) await app.start()
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 => {
@ -180,8 +254,7 @@ const postToTechThermostatChannel = async optionsOrText => {
return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId }) return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId })
} }
const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText) const messageAdmin = async optionsOrText => messageIn(users.Admin, 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') {
@ -194,19 +267,16 @@ const messageIn = async (channel, optionsOrText) => {
const startPoll = async () => { const startPoll = async () => {
const sent = await postToTechThermostatChannel({ const sent = await postToTechThermostatChannel({
text: `<!here|here> Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted.\n` + text: `<!here> Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted.
`Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is.` + Pick :${colderEmoji}: if you want it colder, :${hotterEmoji}: if you want it hotter, or :${goodEmoji}: if you like it how it is.
`\n(Note that I can't actually change the temperature yet. Make Quade do it!)` (Note that I can't actually change the temperature yet. Make ${users.ThermoController} do it!)`
})
await addReactions({
app,
channelId: temperatureChannelId,
timestamp: sent.ts,
reactions: [colderEmoji, hotterEmoji, goodEmoji]
}) })
const addReaction = async emojiName =>
app.client.reactions.add({
channel: temperatureChannelId,
timestamp: sent.ts,
name: emojiName
})
await addReaction(colderEmoji)
await addReaction(hotterEmoji)
await addReaction(goodEmoji)
return sent.ts return sent.ts
} }
@ -218,36 +288,57 @@ 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) => {
const regex = new RegExp(`http://${key}ZZZ[^|]*`) try {
let match = message.match(regex) const regex = new RegExp(`http://${key}ZZZ[^|]*`)
if (!match) { let match = message.match(regex)
return match if (!match) {
return match
}
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
} catch (e) {
console.error(e)
return null
} }
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
} }
const onReaction = listener => reactionListeners.push(listener) const 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 }) => {
if (event.user === sageUserId && event.reaction === 'x') { console.log({ event })
console.log(event) if (event.reaction === 'x' && (event.user === users.Admin || (await wasMyMessage(event)) || await channelIsIm(event.item.channel))) {
try { try {
await app.client.chat.delete({channel: event.item.channel, ts: event.item.ts}) await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
} catch (e) { } catch (e) {
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,
@ -257,10 +348,12 @@ module.exports = {
onReaction, onReaction,
encodeData, encodeData,
decodeData, decodeData,
sageUserId, messageAdmin,
messageSage,
messageIn, messageIn,
testMode, testMode,
testId, testId,
ourUsers users,
buildSayPrepend,
pollTriggers,
pendingRestart
} }