Compare commits
10 Commits
e971f7e7c2
...
969a6b900b
Author | SHA1 | Date |
---|---|---|
Sage Vaillancourt | 969a6b900b | |
Sage Vaillancourt | fdc811d49b | |
Sage Vaillancourt | 7915e3803c | |
Sage Vaillancourt | dcabca0c1a | |
Sage Vaillancourt | be0b49393f | |
Sage Vaillancourt | 4433c19d04 | |
Sage Vaillancourt | ad021cf9a5 | |
Sage Vaillancourt | 6dabe9d85a | |
Sage Vaillancourt | 4e15721803 | |
Sage Vaillancourt | 8577f6f272 |
|
@ -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.
|
File diff suppressed because it is too large
Load Diff
|
@ -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))
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const routine = require('./routine')
|
const routine = require('./routine')
|
||||||
|
|
||||||
const emptyBoard = [
|
const emptyBoard = [
|
||||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',]
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ']
|
||||||
]
|
]
|
||||||
|
|
||||||
const textFromBoard = board => {
|
const textFromBoard = board => {
|
||||||
|
@ -54,7 +54,7 @@ const checkDiagonals = board => {
|
||||||
board[row][col] === board[row + 1][col + 1] &&
|
board[row][col] === board[row + 1][col + 1] &&
|
||||||
board[row][col] === board[row + 2][col + 2] &&
|
board[row][col] === board[row + 2][col + 2] &&
|
||||||
board[row][col] === board[row + 3][col + 3]
|
board[row][col] === board[row + 3][col + 3]
|
||||||
){
|
) {
|
||||||
return board[row][col]
|
return board[row][col]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ const checkDiagonals = board => {
|
||||||
board[row][col] === board[row + 1][col - 1] &&
|
board[row][col] === board[row + 1][col - 1] &&
|
||||||
board[row][col] === board[row + 2][col - 2] &&
|
board[row][col] === board[row + 2][col - 2] &&
|
||||||
board[row][col] === board[row + 3][col - 3]
|
board[row][col] === board[row + 3][col - 3]
|
||||||
){
|
) {
|
||||||
return board[row][col]
|
return board[row][col]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ const checkDiagonals = board => {
|
||||||
const checkFull = board => {
|
const checkFull = board => {
|
||||||
for (const row of board) {
|
for (const row of board) {
|
||||||
for (const col of row) {
|
for (const col of row) {
|
||||||
if (col !== ' ') {
|
if (col === ' ') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,9 +21,24 @@ 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',
|
||||||
|
text: {
|
||||||
|
type: 'plain_text',
|
||||||
|
text: '1',
|
||||||
|
emoji: true
|
||||||
|
},
|
||||||
|
value: 'buy_' + itemName,
|
||||||
|
action_id: 'buy_' + itemName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildBlock2 = ({ user, itemName, cost, cps }) => ({
|
||||||
|
type: 'actions',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: {
|
text: {
|
||||||
type: 'plain_text',
|
type: 'plain_text',
|
||||||
|
@ -41,72 +48,111 @@ const buildBlock = ({ user, itemName, cost, cps }) => ({
|
||||||
value: 'buy_' + itemName,
|
value: 'buy_' + itemName,
|
||||||
action_id: 'buy_' + itemName
|
action_id: 'buy_' + itemName
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: {
|
||||||
|
type: 'plain_text',
|
||||||
|
text: 'Buy 1',
|
||||||
|
emoji: true
|
||||||
|
},
|
||||||
|
value: 'buy_' + itemName,
|
||||||
|
action_id: 'buy_' + itemName
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const buyText2 = (highestCoins, user) => {
|
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: [
|
||||||
|
{
|
||||||
|
type: 'section',
|
||||||
|
text: {
|
||||||
|
type: 'mrkdwn',
|
||||||
|
text: (extraMessage && extraMessage + '\n')
|
||||||
|
+ `You have ${commas(user.coins)} HVAC to spend.\n`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...Object.entries(buyableItems)
|
||||||
|
.filter(([, item]) => canView(item, highestCoins))
|
||||||
|
.map(([itemName]) => {
|
||||||
const cost = calculateCost({ itemName, user, quantity: 1 })
|
const cost = calculateCost({ itemName, user, quantity: 1 })
|
||||||
const cps = Math.round(singleItemCps(user, itemName))
|
const cps = Math.round(singleItemCps(user, itemName))
|
||||||
return ({ user, itemName, cost, cps })
|
return ({ user, itemName, cost, cps })
|
||||||
}).map(buildBlock)
|
}).map(buildBlock)
|
||||||
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const 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 }
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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
|
|
@ -1,56 +1,54 @@
|
||||||
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 user = getUser(event.user)
|
|
||||||
getCoins(event.user)
|
|
||||||
const possible = possiblePrestige(user.coinsAllTime)
|
const possible = possiblePrestige(user.coinsAllTime)
|
||||||
const current = user.prestige ??= 0
|
const current = user.prestige ??= 0
|
||||||
if (words[1] === 'me') {
|
if (args[0] === 'me') {
|
||||||
await say(
|
await say(
|
||||||
'This will permanently remove all of your items, upgrades, and coins!\n\n' +
|
'This will permanently remove all of your items, upgrades, and coins!\n\n' +
|
||||||
'Say \'!!prestige me\' to confirm.'
|
'Say \'!!prestige me\' to confirm.'
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
const currentCost = totalCostForPrestige(possible)
|
||||||
|
const nextCost = totalCostForPrestige(possible + 1)
|
||||||
|
const diff = nextCost - currentCost
|
||||||
|
const progress = user.coinsAllTime - currentCost
|
||||||
|
const bars = Math.round((progress / diff) * 10)
|
||||||
|
const empty = 10 - bars
|
||||||
|
const progressBar = '[' + '='.repeat(bars) + ' '.repeat(empty) + ']'
|
||||||
await say(
|
await say(
|
||||||
`Current Prestige: ${commas(current)}\n\n` +
|
`Current Prestige: ${commas(current)}\n\n` +
|
||||||
`Quacks gained if you prestige now: ${commas(possible - current)}\n\n` +
|
`Quacks gained if you prestige now: ${commas(possible - current)}\n\n` +
|
||||||
`HVAC until next quack: ${commas(Math.round(tpcRec(possible + 1) - user.coinsAllTime))}\n\n` +
|
`Next quack progress: \`${progressBar} ${commas(diff)} \`\n\n` +
|
||||||
'Say \'!prestige me\' to start the prestige process.'
|
'Say \'!prestige me\' to start the prestige process.' +
|
||||||
|
`\n\nYour prestige is currently boosting your CPS by ${commas((prestigeMultiplier(user) - 1) * 100)}%`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}//, true, adminOnly)
|
}//, 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) {
|
||||||
|
@ -61,65 +59,243 @@ const prestigeConfirmRoute = async ({ event, say, words }) => {
|
||||||
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.isPrestiging = true
|
||||||
|
|
||||||
user.quacks ??= 0
|
user.quacks ??= 0
|
||||||
user.quacks += (possible - user.prestige)
|
user.quacks += (possible - user.prestige)
|
||||||
console.log('user.quacks', user.quacks)
|
|
||||||
user.prestige = possible
|
user.prestige = possible
|
||||||
|
user.highestEver = 0
|
||||||
user.coins = 0
|
user.coins = 0
|
||||||
user.items = {}
|
user.items = {}
|
||||||
|
user.holdings = {}
|
||||||
|
const starterUpgrades = (user.quackUpgrades?.starter || [])
|
||||||
|
starterUpgrades.forEach(upgradeName => quackStore[upgradeName].effect(user))
|
||||||
user.upgrades = {}
|
user.upgrades = {}
|
||||||
saveGame()
|
|
||||||
await say('You prestiged! Check out !quackstore to see what you can buy!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const quackStoreListing = ([name, upgrade]) =>
|
await say(prestigeMenu(user))
|
||||||
`:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
horrorEnabled: false,
|
||||||
|
admins: ['Sage']
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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: {},
|
users: {},
|
||||||
nfts: [],
|
nfts: [],
|
||||||
squad: {}
|
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
|
||||||
|
if (force || saves % 10 === 0) {
|
||||||
|
if (after) {
|
||||||
|
console.log(`SAVING GAME after ${after}`)
|
||||||
|
} else {
|
||||||
|
console.log('SAVING GAME')
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2))
|
fs.writeFileSync('./' + saveFile, JSON.stringify(game, null, 2))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeNews = say => {
|
const maybeNews = say => {
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
} else {
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUser = (userId, updateCoins = false) => {
|
||||||
|
users[userId] ??= {}
|
||||||
|
users[userId].coins ??= 0
|
||||||
users[userId].items ??= {}
|
users[userId].items ??= {}
|
||||||
users[userId].upgrades ??= {}
|
users[userId].upgrades ??= {}
|
||||||
users[userId].achievements ??= {}
|
users[userId].achievements ??= {}
|
||||||
users[userId].coinsAllTime ??= users[userId].coins
|
users[userId].coinsAllTime ??= users[userId].coins
|
||||||
users[userId].prestige ??= 0
|
users[userId].prestige ??= 0
|
||||||
|
users[userId].startDate ??= new Date()
|
||||||
|
if (updateCoins) {
|
||||||
|
users[userId].coins = getCoins(userId)
|
||||||
}
|
}
|
||||||
return users[userId]
|
return users[userId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addCoins = (user, add) => {
|
||||||
|
user.coins += add
|
||||||
|
user.coinsAllTime += add
|
||||||
|
user.coinsAllTime = Math.floor(user.coinsAllTime)
|
||||||
|
user.coins = Math.floor(user.coins)
|
||||||
|
}
|
||||||
|
|
||||||
const getCoins = userId => {
|
const getCoins = userId => {
|
||||||
const user = getUser(userId)
|
const user = getUser(userId)
|
||||||
const currentTime = getSeconds()
|
const currentTime = getSeconds()
|
||||||
const lastCheck = user.lastCheck || currentTime
|
const lastCheck = user.lastCheck || currentTime
|
||||||
const secondsPassed = currentTime - lastCheck
|
const secondsPassed = currentTime - lastCheck
|
||||||
|
|
||||||
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,31 +320,234 @@ const getCompletedSquadgrades = () =>
|
||||||
.filter(squadHas)
|
.filter(squadHas)
|
||||||
.map(([, upgrade]) => upgrade)
|
.map(([, upgrade]) => upgrade)
|
||||||
|
|
||||||
|
const getCompletedSquadgradeNames = () =>
|
||||||
|
Object.entries(squadUpgrades)
|
||||||
|
.filter(squadHas)
|
||||||
|
.map(([name]) => name)
|
||||||
|
|
||||||
|
const prestigeMultiplier = user => 1 + ((user.prestige || 0) * 0.01)
|
||||||
|
|
||||||
|
const quackGradeMultiplier = user => {
|
||||||
|
const userQuackgrades = user.quackUpgrades?.cps || []
|
||||||
|
return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const petQuackGradeMultiplier = user => {
|
||||||
|
const userQuackgrades = user.quackUpgrades?.pet || []
|
||||||
|
return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), petBoost())
|
||||||
|
}
|
||||||
|
|
||||||
const singleItemCps = (user, itemName) => {
|
const singleItemCps = (user, itemName) => {
|
||||||
const baseCps = buyableItems[itemName].earning
|
const baseCps = buyableItems[itemName].earning
|
||||||
|
// console.log('')
|
||||||
|
// console.log(`${itemName} CPS:`)
|
||||||
|
// console.log('baseCps', baseCps)
|
||||||
|
|
||||||
const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name])
|
const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name])
|
||||||
const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), 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
|
||||||
}
|
}
|
|
@ -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 =>
|
||||||
|
|
|
@ -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 => {
|
||||||
|
try {
|
||||||
await slack.app.client.reactions.add({
|
await slack.app.client.reactions.add({
|
||||||
channel,
|
channel,
|
||||||
timestamp: ts,
|
timestamp: ts,
|
||||||
name: emojiName
|
name: emojiName
|
||||||
})
|
})
|
||||||
|
} catch (ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const choice of choices) {
|
for (const choice of choices) {
|
||||||
await addEmoji(choice)
|
await addEmoji(choice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => {
|
const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => {
|
||||||
if (event.channel_type === 'im') {
|
if (event.channel_type !== 'im') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const eventText = event.text?.toLowerCase()
|
const eventText = event.text?.toLowerCase()
|
||||||
if (eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword))) {
|
if (!(eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword)))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
console.log('Trigger found')
|
||||||
const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0]
|
const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0]
|
||||||
const msg = messageFromBoard({
|
console.log('Messaging opponent ' + slack.users[opponent.substring(2, opponent.length - 1)])
|
||||||
|
const channelMap = {}
|
||||||
|
const msg = () => messageFromBoard({
|
||||||
dataName,
|
dataName,
|
||||||
gameName,
|
gameName,
|
||||||
textFromBoard,
|
textFromBoard,
|
||||||
board: initialBoard(),
|
board: initialBoard(),
|
||||||
player1: '<@' + event.user + '>',
|
player1: '<@' + event.user + '>',
|
||||||
player2: opponent
|
player2: opponent,
|
||||||
|
channelMap
|
||||||
})
|
})
|
||||||
const sent = await say(msg)
|
const sent = await say(msg())
|
||||||
await addChoiceEmojis({ ...sent, choices: turnChoiceEmojis })
|
channelMap[event.user] = {
|
||||||
|
channel: sent.channel,
|
||||||
|
ts: sent.ts
|
||||||
}
|
}
|
||||||
|
await updateAll({ name: gameName, text: msg(), add: sent })
|
||||||
|
await addChoiceEmojis({...sent, choices: turnChoiceEmojis})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, { board, players })
|
const encodeGame = (dataKey, board, players, channelMap = {}) => slack.encodeData(dataKey, { board, players, channelMap })
|
||||||
|
|
||||||
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
|
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
|
||||||
|
|
||||||
|
@ -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...')
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeEmoji = async emojiName =>
|
|
||||||
slack.app.client.reactions.remove({
|
slack.app.client.reactions.remove({
|
||||||
channel: event.item.channel,
|
channel,
|
||||||
timestamp: message.messages[0]?.ts,
|
timestamp: ts,
|
||||||
name: emojiName
|
name: emojiName
|
||||||
})
|
}))
|
||||||
turnChoiceEmojis.forEach(removeEmoji)
|
turnChoiceEmojis.forEach(removeEmoji)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const winnerMessages = getMessages(winner)
|
||||||
|
// await say(boardMessage() + winnerMessages.you)
|
||||||
|
console.log('TurnHandler', { gameName, boardMessage: boardMessage() })
|
||||||
|
// await updateAll({ name: gameName, text: boardMessage() + '\nTurnHandler' })
|
||||||
|
// if (!winner) {
|
||||||
|
// await say('Waiting for opponent\'s response...')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const removeEmoji = async emojiName =>
|
||||||
|
// slack.app.client.reactions.remove({
|
||||||
|
// channel: event.item.channel,
|
||||||
|
// timestamp: message.messages[0]?.ts,
|
||||||
|
// name: emojiName
|
||||||
|
// })
|
||||||
|
// turnChoiceEmojis.forEach(removeEmoji)
|
||||||
|
console.log('SENDING to ' + opponent)
|
||||||
|
if (!channelMap[opponent]) {
|
||||||
const sentBoard = await slack.app.client.chat.postMessage({
|
const sentBoard = await slack.app.client.chat.postMessage({
|
||||||
channel: opponent,
|
channel: opponent,
|
||||||
text: boardMessage + winnerMessages.opponent
|
text: boardMessage() + winnerMessages.opponent
|
||||||
})
|
})
|
||||||
|
channelMap[opponent] = {
|
||||||
|
channel: sentBoard.channel,
|
||||||
|
ts: sentBoard.ts
|
||||||
|
}
|
||||||
|
console.log('BOARD MESSAGE AFTER ')
|
||||||
|
await updateAll({ name: gameName, text: boardMessage(), add: sentBoard })
|
||||||
|
} else {
|
||||||
|
await updateAll({ name: gameName, text: boardMessage() })
|
||||||
|
}
|
||||||
if (!winner) {
|
if (!winner) {
|
||||||
|
const sentBoard = channelMap[opponent]
|
||||||
await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis })
|
await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
10
src/index.js
10
src/index.js
|
@ -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
|
||||||
|
|
|
@ -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!)`
|
||||||
})
|
})
|
||||||
const addReaction = async emojiName =>
|
await addReactions({
|
||||||
app.client.reactions.add({
|
app,
|
||||||
channel: temperatureChannelId,
|
channelId: temperatureChannelId,
|
||||||
timestamp: sent.ts,
|
timestamp: sent.ts,
|
||||||
name: emojiName
|
reactions: [colderEmoji, hotterEmoji, goodEmoji]
|
||||||
})
|
})
|
||||||
await addReaction(colderEmoji)
|
|
||||||
await addReaction(hotterEmoji)
|
|
||||||
await addReaction(goodEmoji)
|
|
||||||
return sent.ts
|
return sent.ts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,10 +288,12 @@ const requestTempChange = change => {
|
||||||
tempChangeListeners.forEach(listener => listener(change))
|
tempChangeListeners.forEach(listener => listener(change))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noinspection HttpUrlsUsage
|
||||||
const encodeData = (key, data) =>
|
const encodeData = (key, data) =>
|
||||||
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
|
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
|
||||||
|
|
||||||
const decodeData = (key, message) => {
|
const decodeData = (key, message) => {
|
||||||
|
try {
|
||||||
const regex = new RegExp(`http://${key}ZZZ[^|]*`)
|
const regex = new RegExp(`http://${key}ZZZ[^|]*`)
|
||||||
let match = message.match(regex)
|
let match = message.match(regex)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
@ -229,25 +301,44 @@ const decodeData = (key, message) => {
|
||||||
}
|
}
|
||||||
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
|
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
|
||||||
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
|
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReaction = listener => reactionListeners.push(listener)
|
const onReaction = listener => reactionListeners.push(listener)
|
||||||
|
|
||||||
|
const channelIsIm = async channel => (await app.client.conversations.info({ channel }))?.channel?.is_im
|
||||||
|
|
||||||
|
const wasMyMessage = async event => {
|
||||||
|
const text = (await app.client.conversations.history({
|
||||||
|
channel: event.item.channel,
|
||||||
|
latest: event.item.ts,
|
||||||
|
limit: 1,
|
||||||
|
inclusive: true
|
||||||
|
})).messages[0].text
|
||||||
|
|
||||||
|
const decoded = decodeData('commandPayload', text)
|
||||||
|
return decoded.event.user === event.user
|
||||||
|
}
|
||||||
|
|
||||||
onReaction(async ({ event }) => {
|
onReaction(async ({ event }) => {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue