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()
|
||||
headers.append('Authorization', 'Basic ' + base64.encode(config.honeywellKey + ':' + config.honeywellSecret))
|
||||
|
||||
fetch(url, {method:'GET',
|
||||
headers: headers,
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
// credentials: 'user:passwd'
|
||||
}).then(response => response.json())
|
||||
.then(json => console.log(json));
|
||||
.then(json => console.log('json', json))
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
const routine = require('./routine')
|
||||
|
||||
const emptyBoard = [
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ',]
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ']
|
||||
]
|
||||
|
||||
const textFromBoard = board => {
|
||||
|
@ -76,7 +76,7 @@ const checkDiagonals = board => {
|
|||
const checkFull = board => {
|
||||
for (const row of board) {
|
||||
for (const col of row) {
|
||||
if (col !== ' ') {
|
||||
if (col === ' ') {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ module.exports = {
|
|||
leaderBoardViewer: {
|
||||
name: 'Leaderboard-Viewer',
|
||||
description: 'Thank you for viewing the leaderboard!',
|
||||
emoji: 'trophy',
|
||||
emoji: 'trophy'
|
||||
},
|
||||
seeTheQuade: {
|
||||
name: 'See the Quade',
|
||||
description: 'Quade has appeared in your buyables',
|
||||
emoji: 'quade',
|
||||
emoji: 'quade'
|
||||
},
|
||||
greenCoin: {
|
||||
name: 'Lucky Green Coin',
|
||||
|
@ -34,6 +34,21 @@ module.exports = {
|
|||
description: 'I like big bets, and that\'s the truth',
|
||||
emoji: 'slot_machine'
|
||||
},
|
||||
hugeBets: {
|
||||
name: 'Make a bet over 100T',
|
||||
description: `That's so bonk`,
|
||||
emoji: 'game_die'
|
||||
},
|
||||
mondoBets: {
|
||||
name: 'Make a bet over 100 Quadrillion',
|
||||
description: 'H I G H R O L L E R',
|
||||
emoji: '8ball'
|
||||
},
|
||||
sigmaBets: {
|
||||
name: 'Make a bet over 100 Quintillion',
|
||||
description: 'Return to monke',
|
||||
emoji: 'yeknom'
|
||||
},
|
||||
ignited: {
|
||||
name: 'You light my fire, baby',
|
||||
description: 'And you pay attention to descriptions!',
|
||||
|
@ -45,8 +60,79 @@ module.exports = {
|
|||
description: 'I\'m beginning to feel like a rat god, rat god.',
|
||||
emoji: 'mouse2'
|
||||
},
|
||||
mathematician: {
|
||||
name: 'Own 100 Accountants',
|
||||
description: 'They rejoice at the appearance of a third digit.',
|
||||
emoji: 'male-office-worker'
|
||||
},
|
||||
iPod: {
|
||||
name: 'Own 100 Whales',
|
||||
description: `With the new iPod, you can hold 100's of songs.`,
|
||||
emoji: 'whale'
|
||||
},
|
||||
fire100: {
|
||||
name: 'Own 100 Fires',
|
||||
description: `Wow, that's bright.`,
|
||||
emoji: 'fire'
|
||||
},
|
||||
train100: {
|
||||
name: 'Own 100 Trains',
|
||||
description: `That's every train in America you've got there.`,
|
||||
emoji: 'train2'
|
||||
},
|
||||
boom100: {
|
||||
name: 'Own 100 Boomerangs',
|
||||
description: `LOUD WOOSHING`,
|
||||
emoji: 'boomerang'
|
||||
},
|
||||
moon100: {
|
||||
name: 'Own 100 Moons',
|
||||
description: `Space Cadet`,
|
||||
emoji: 'new_moon_with_face'
|
||||
},
|
||||
mirror100: {
|
||||
name: 'Own 100 Mirrors',
|
||||
description: `Disco Ball`,
|
||||
emoji: 'mirror'
|
||||
},
|
||||
butterfly100: {
|
||||
name: 'Own 100 Butterflies',
|
||||
description: `Delicate yet powerful.`,
|
||||
emoji: 'butterfly'
|
||||
},
|
||||
quade100: {
|
||||
name: 'Own 100 Quades',
|
||||
description: `Your Ops are super Devved right now.`,
|
||||
emoji: 'quade'
|
||||
},
|
||||
hvacker100: {
|
||||
name: 'Own 100 Hvackers',
|
||||
description: `Did Sage finally make his git repo public?`,
|
||||
emoji: 'hvacker_angery'
|
||||
},
|
||||
creator100: {
|
||||
name: 'Own 100 Creators',
|
||||
description: `_Stern look_`,
|
||||
emoji: 'question'
|
||||
},
|
||||
smallBusiness100: {
|
||||
name: 'Own 100 Small Businesses',
|
||||
description: `Enough to run a small city.`,
|
||||
emoji: 'convenience_store'
|
||||
},
|
||||
bigBusiness100: {
|
||||
name: 'Own 100 Big Businesses',
|
||||
description: `I mean... that's basically all of them.`,
|
||||
emoji: 'office'
|
||||
},
|
||||
government100: {
|
||||
name: 'Run 100 Governments',
|
||||
description: `I hope you're using them for something good...`,
|
||||
emoji: 'japanese_castle'
|
||||
},
|
||||
|
||||
weAllNeedHelp: {
|
||||
name: `View the '!coin' help`,
|
||||
name: 'View the \'!coin\' help',
|
||||
description: 'We all need a little help sometimes',
|
||||
emoji: 'grey_question'
|
||||
},
|
||||
|
@ -71,9 +157,25 @@ module.exports = {
|
|||
description: 'You absolutely know how to party.',
|
||||
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: {
|
||||
name: 'You disgust me',
|
||||
description: 'Like, wow.',
|
||||
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 { commas, saveGame, setHighestCoins, addAchievement, getCoins, getUser, singleItemCps } = require('./utils');
|
||||
const buyableItems = require('./buyableItems')
|
||||
const { commas, setHighestCoins, addAchievement, getUser, singleItemCps, chaosFilter, fuzzyMatcher, calculateCost } = require('./utils')
|
||||
const slack = require('../../slack')
|
||||
|
||||
const calculateCost = ({ itemName, user, quantity = 1 }) => {
|
||||
let currentlyOwned = user.items[itemName] || 0
|
||||
let realCost = 0
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
realCost += Math.ceil(buyableItems[itemName].baseCost * Math.pow(1.15, currentlyOwned || 0))
|
||||
currentlyOwned += 1
|
||||
}
|
||||
return realCost
|
||||
}
|
||||
const leaderboardUpdater = {}
|
||||
|
||||
const getItemHeader = user => ([itemName, { baseCost, description, emoji }]) => {
|
||||
const itemCost = commas(user ? calculateCost({ itemName, user }) : baseCost)
|
||||
const itemCps = Math.round(singleItemCps(user, itemName))
|
||||
return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(itemCps)} CPS\n_${description}_`
|
||||
}
|
||||
const canView = highestCoins => ([, item]) => item.baseCost < (highestCoins || 1) * 101
|
||||
const canView = (item, highestCoins) => item.baseCost < (highestCoins || 1) * 101
|
||||
const buyableText = (highestCoins, user) => Object.entries(buyableItems)
|
||||
.filter(canView(highestCoins))
|
||||
.filter(([, item]) => canView(item, highestCoins))
|
||||
.map(getItemHeader(user))
|
||||
.join('\n\n') +
|
||||
'\n\n:grey_question::grey_question::grey_question:' +
|
||||
|
@ -29,9 +21,24 @@ const buildBlock = ({ user, itemName, cost, cps }) => ({
|
|||
type: 'section',
|
||||
text: {
|
||||
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: {
|
||||
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',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
|
@ -41,72 +48,111 @@ const buildBlock = ({ user, itemName, cost, cps }) => ({
|
|||
value: 'buy_' + itemName,
|
||||
action_id: 'buy_' + itemName
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Buy 1',
|
||||
emoji: true
|
||||
},
|
||||
value: 'buy_' + itemName,
|
||||
action_id: 'buy_' + itemName
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const buyText2 = (highestCoins, user) => {
|
||||
const buyText2 = (highestCoins, user, extraMessage = '') => {
|
||||
return ({
|
||||
text: buyableText(highestCoins, user),
|
||||
blocks: Object.entries(buyableItems)
|
||||
.filter(canView(highestCoins))
|
||||
.map(([itemName, item]) => {
|
||||
text: (extraMessage && extraMessage + '\n')
|
||||
+ `You have ${commas(user.coins)} HVAC to spend.\n`
|
||||
+ buyableText(highestCoins, user),
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: (extraMessage && extraMessage + '\n')
|
||||
+ `You have ${commas(user.coins)} HVAC to spend.\n`
|
||||
},
|
||||
},
|
||||
...Object.entries(buyableItems)
|
||||
.filter(([, item]) => canView(item, highestCoins))
|
||||
.map(([itemName]) => {
|
||||
const cost = calculateCost({ itemName, user, quantity: 1 })
|
||||
const cps = Math.round(singleItemCps(user, itemName))
|
||||
return ({ user, itemName, cost, cps })
|
||||
}).map(buildBlock)
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const buyRoute = async ({ event, say, words }) => {
|
||||
const user = getUser(event.user)
|
||||
const buying = words[1]
|
||||
const maxQuantity = ({ itemName, user, currentCoins }) => {
|
||||
let quantity = 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)
|
||||
const query = event?.text?.startsWith('?b ') || event?.text?.startsWith('?buy ')
|
||||
|
||||
if (!buying) {
|
||||
const highestCoins = user.highestEver || user.coins || 1
|
||||
if (buyableItems.quade.baseCost < highestCoins * 100) {
|
||||
if (canView(buyableItems.quade, highestCoins)) {
|
||||
addAchievement(user, 'seeTheQuade', say)
|
||||
}
|
||||
await say(buyText2(highestCoins, user))
|
||||
return
|
||||
}
|
||||
|
||||
const buyable = buyableItems[buying]
|
||||
const matcher = fuzzyMatcher(buying)
|
||||
const buyable = Object.entries(buyableItems).find(([name]) => matcher.test(name))
|
||||
if (!buyable) {
|
||||
await say('That item does not exist!')
|
||||
return
|
||||
}
|
||||
|
||||
let quantity = 1
|
||||
const currentCoins = getCoins(event.user)
|
||||
if (words[2] === 'max') {
|
||||
while (calculateCost({ itemName: buying, user, quantity: quantity + 1 }) <= currentCoins) {
|
||||
quantity++
|
||||
}
|
||||
const [buyableName, buyableItem] = buyable
|
||||
let quantity
|
||||
const currentCoins = user.coins
|
||||
const max = maxQuantity({ itemName: buyableName, user, currentCoins })
|
||||
if (!args[1]) {
|
||||
quantity = 1
|
||||
} else if (args[1] === 'max') {
|
||||
quantity = max
|
||||
} 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) {
|
||||
await say('Quantity must be a positive integer')
|
||||
return
|
||||
}
|
||||
|
||||
const realCost = calculateCost({ itemName: buying, user, quantity })
|
||||
const realCost = calculateCost({ itemName: buyableName, user, quantity })
|
||||
if (query) {
|
||||
return say(`Buying ${quantity} ${buyableName} would cost you ${commas(realCost)} HVAC`)
|
||||
}
|
||||
if (currentCoins < realCost) {
|
||||
await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`)
|
||||
await say(`You don't have enough coins! You need ${commas(realCost)}`)
|
||||
return
|
||||
}
|
||||
user.coins -= realCost
|
||||
user.items[buying] = user.items[buying] || 0
|
||||
user.items[buying] += quantity
|
||||
console.log(buying, user.items.mouse)
|
||||
if (buying === 'mouse' && user.items.mouse >= 100) {
|
||||
addAchievement(user, 'ratGod', say)
|
||||
user.items[buyableName] = user.items[buyableName] || 0
|
||||
user.items[buyableName] += quantity
|
||||
|
||||
if (user.items[buyableName] >= 100) {
|
||||
addAchievement(user, buyableItems[buyableName].own100Achievement, say)
|
||||
}
|
||||
if (quantity === 1) {
|
||||
await say(`You bought one :${buyable.emoji}:`)
|
||||
} else {
|
||||
await say(`You bought ${quantity} :${buyable.emoji}:`)
|
||||
}
|
||||
saveGame()
|
||||
|
||||
const countString = quantity === 1 ? 'one' : quantity
|
||||
await say(`You bought ${countString} :${buyableItem.emoji}:`)
|
||||
}
|
||||
|
||||
const buyButton = async ({ body, ack, say, payload }) => {
|
||||
|
@ -114,19 +160,25 @@ const buyButton = async ({ body, ack, say, payload }) => {
|
|||
const buying = payload.action_id.substring(4)
|
||||
console.log(`buyButton ${buying} clicked`)
|
||||
const event = {
|
||||
user: body.user.id,
|
||||
user: body.user.id
|
||||
}
|
||||
const user = getUser(event.user)
|
||||
const words = ['', buying, '1']
|
||||
await buyRoute({ event, say, words })
|
||||
const words = ['', buying, body.actions[0].text]
|
||||
const [commandName, ...args] = words
|
||||
|
||||
let extraMessage = ''
|
||||
say = async text => extraMessage = text
|
||||
await buyRoute({ event, say, words, args, commandName, user })
|
||||
|
||||
const highestCoins = user.highestEver || user.coins || 1
|
||||
await slack.app.client.chat.update({
|
||||
channel: body.channel.id,
|
||||
ts: body.message.ts,
|
||||
...buyText2(highestCoins, user)
|
||||
...buyText2(highestCoins, user, extraMessage)
|
||||
})
|
||||
await leaderboardUpdater.updateAllLeaderboards()
|
||||
}
|
||||
|
||||
Object.keys(buyableItems).forEach(itemName => slack.app.action('buy_' + itemName, buyButton))
|
||||
|
||||
module.exports = buyRoute
|
||||
module.exports = { buyRoute, leaderboardUpdater }
|
||||
|
|
|
@ -3,72 +3,105 @@ module.exports = {
|
|||
baseCost: 100,
|
||||
earning: 1,
|
||||
emoji: 'mouse2',
|
||||
description: 'A mouse to steal coins for you.'
|
||||
description: 'A mouse to steal coins for you.',
|
||||
own100Achievement: 'ratGod',
|
||||
},
|
||||
accountant: {
|
||||
baseCost: 1_100,
|
||||
earning: 8,
|
||||
emoji: 'male-office-worker',
|
||||
description: 'Legally make money from nothing!'
|
||||
description: 'Legally make money from nothing!',
|
||||
own100Achievement: 'mathematician',
|
||||
},
|
||||
whale: {
|
||||
baseCost: 12_000,
|
||||
earning: 47,
|
||||
emoji: 'whale',
|
||||
description: 'Someone to spend money on your HVAC Coin mining app.'
|
||||
description: 'Someone to spend money on your HVAC Coin mining app.',
|
||||
own100Achievement: 'iPod',
|
||||
},
|
||||
train: {
|
||||
baseCost: 130_000,
|
||||
earning: 260,
|
||||
emoji: 'train2',
|
||||
description: 'Efficiently ship your most valuable coins.'
|
||||
description: 'Efficiently ship your most valuable coins.',
|
||||
own100Achievement: 'train100',
|
||||
},
|
||||
fire: {
|
||||
baseCost: 1_400_000,
|
||||
earning: 1_400,
|
||||
emoji: 'fire',
|
||||
description: 'Return to the roots of HVAC.'
|
||||
description: 'Return to the roots of HVAC.',
|
||||
own100Achievement: 'fire100',
|
||||
},
|
||||
boomerang: {
|
||||
baseCost: 20_000_000,
|
||||
earning: 7_800,
|
||||
emoji: 'boomerang',
|
||||
description: 'Your coin always seems to come back.'
|
||||
description: 'Your coin always seems to come back.',
|
||||
own100Achievement: 'boom100',
|
||||
},
|
||||
moon: {
|
||||
baseCost: 330_000_000,
|
||||
earning: 44_000,
|
||||
emoji: 'new_moon_with_face',
|
||||
description: 'Convert dark new-moon energy into HVAC Coins.'
|
||||
description: 'Convert dark new-moon energy into HVAC Coins.',
|
||||
own100Achievement: 'moon100',
|
||||
},
|
||||
butterfly: {
|
||||
baseCost: 5_100_000_000,
|
||||
earning: 260_000,
|
||||
emoji: 'butterfly',
|
||||
description: 'Create the exact worldly chaos to bit-flip HVAC Coins into existence on your computer.'
|
||||
description: 'Create the exact worldly chaos to bit-flip HVAC Coins into existence on your computer.',
|
||||
own100Achievement: 'butterfly100',
|
||||
},
|
||||
mirror: {
|
||||
baseCost: 75_000_000_000,
|
||||
earning: 1_600_000,
|
||||
emoji: 'mirror',
|
||||
description: 'Only by gazing inward can you collect enough Coin to influence the thermostat.'
|
||||
description: 'Only by gazing inward can you collect enough Coin to influence the thermostat.',
|
||||
own100Achievement: 'mirror100',
|
||||
},
|
||||
quade: {
|
||||
baseCost: 1_000_000_000_000,
|
||||
earning: 10_000_000,
|
||||
emoji: 'quade',
|
||||
description: 'Has thumbs capable of physically manipulating the thermostat.'
|
||||
description: 'Has thumbs capable of physically manipulating the thermostat.',
|
||||
own100Achievement: 'quade100',
|
||||
},
|
||||
hvacker: {
|
||||
baseCost: 14_000_000_000_000,
|
||||
earning: 65_000_000,
|
||||
emoji: 'hvacker_angery',
|
||||
description: 'Harness the power of the mad god himself.'
|
||||
description: 'Harness the power of the mad god himself.',
|
||||
own100Achievement: 'hvacker100',
|
||||
},
|
||||
creator: {
|
||||
baseCost: 170_000_000_000_000,
|
||||
earning: 430_000_000,
|
||||
emoji: 'question',
|
||||
description: 'The elusive creator of Hvacker takes a favorable look at your CPS.'
|
||||
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 quackStore = require('./quackstore');
|
||||
const { commas, quackGradeMultiplier, prestigeMultiplier, makeBackup, userHasCheckedQuackgrade, getUser } = require('./utils')
|
||||
const { quackStore } = require('./quackstore')
|
||||
const buyableItems = require('./buyableItems')
|
||||
const slack = require('../../slack')
|
||||
|
||||
const possiblePrestige = coins => {
|
||||
let p = 0
|
||||
while (tpcRec(p + 1) <= coins) {
|
||||
while (totalCostForPrestige(p + 1) <= coins) {
|
||||
p += 1
|
||||
}
|
||||
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 tpcRec = prestigeLevel => {
|
||||
const totalCostForPrestige = prestigeLevel => {
|
||||
if (prestigeLevel === 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 ({ event, say, words }) => {
|
||||
const user = getUser(event.user)
|
||||
getCoins(event.user)
|
||||
const prestigeRoute = async ({ say, args, user }) => {
|
||||
const possible = possiblePrestige(user.coinsAllTime)
|
||||
const current = user.prestige ??= 0
|
||||
if (words[1] === 'me') {
|
||||
if (args[0] === 'me') {
|
||||
await say(
|
||||
'This will permanently remove all of your items, upgrades, and coins!\n\n' +
|
||||
'Say \'!!prestige me\' to confirm.'
|
||||
)
|
||||
} else {
|
||||
const currentCost = totalCostForPrestige(possible)
|
||||
const nextCost = totalCostForPrestige(possible + 1)
|
||||
const diff = nextCost - currentCost
|
||||
const progress = user.coinsAllTime - currentCost
|
||||
const bars = Math.round((progress / diff) * 10)
|
||||
const empty = 10 - bars
|
||||
const progressBar = '[' + '='.repeat(bars) + ' '.repeat(empty) + ']'
|
||||
await say(
|
||||
`Current Prestige: ${commas(current)}\n\n` +
|
||||
`Quacks gained if you prestige now: ${commas(possible - current)}\n\n` +
|
||||
`HVAC until next quack: ${commas(Math.round(tpcRec(possible + 1) - user.coinsAllTime))}\n\n` +
|
||||
'Say \'!prestige me\' to start the prestige process.'
|
||||
`Next quack progress: \`${progressBar} ${commas(diff)} \`\n\n` +
|
||||
'Say \'!prestige me\' to start the prestige process.' +
|
||||
`\n\nYour prestige is currently boosting your CPS by ${commas((prestigeMultiplier(user) - 1) * 100)}%`
|
||||
)
|
||||
}
|
||||
}//, true, adminOnly)
|
||||
|
||||
// TODO
|
||||
const prestigeConfirmRoute = async ({ event, say, words }) => {
|
||||
const user = getUser(event.user)
|
||||
getCoins(event.user)
|
||||
const prestigeConfirmRoute = async ({ event, say, user, YEET }) => {
|
||||
if (YEET) {
|
||||
return say(prestigeMenu(user))
|
||||
}
|
||||
const possible = possiblePrestige(user.coinsAllTime)
|
||||
const current = user.prestige
|
||||
if (possible <= current) {
|
||||
|
@ -61,65 +59,243 @@ const prestigeConfirmRoute = async ({ event, say, words }) => {
|
|||
await say('Say exactly \'!!prestige me\' to confirm')
|
||||
return
|
||||
}
|
||||
console.log('possible', possible)
|
||||
console.log('user.prestige', user.prestige)
|
||||
await makeBackup()
|
||||
|
||||
user.isPrestiging = true
|
||||
|
||||
user.quacks ??= 0
|
||||
user.quacks += (possible - user.prestige)
|
||||
console.log('user.quacks', user.quacks)
|
||||
|
||||
user.prestige = possible
|
||||
user.highestEver = 0
|
||||
user.coins = 0
|
||||
user.items = {}
|
||||
user.holdings = {}
|
||||
const starterUpgrades = (user.quackUpgrades?.starter || [])
|
||||
starterUpgrades.forEach(upgradeName => quackStore[upgradeName].effect(user))
|
||||
user.upgrades = {}
|
||||
saveGame()
|
||||
await say('You prestiged! Check out !quackstore to see what you can buy!')
|
||||
|
||||
await say(prestigeMenu(user))
|
||||
await say(`Say !quack _upgrade-name_ to purchase new quackgrades!`)
|
||||
//await say('You prestiged! Check out !quackstore to see what you can buy!')
|
||||
}
|
||||
|
||||
const quackStoreListing = ([name, upgrade]) =>
|
||||
`:${upgrade.emoji}: *${name}* - Costs *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
|
||||
const quackStoreListing = (showCost = true) => ([name, upgrade]) =>
|
||||
`:${upgrade.emoji}: *${name}* - ${showCost ? 'Costs' : 'Worth'} *${upgrade.cost} Quack.*\n\n_${upgrade.description}_`
|
||||
|
||||
const allUserQuackUpgrades = user =>
|
||||
Object.entries(user.quackUpgrades || {})
|
||||
.map(([type, upgrades]) => upgrades)
|
||||
.map(([type, upgrades]) => upgrades).flatMap(x => x)
|
||||
|
||||
const hasPreReqs = user => ([name, upgrade]) => {
|
||||
if (!upgrade.preReqs) {
|
||||
return true
|
||||
}
|
||||
const allUserUpgrades = allUserQuackUpgrades(user)
|
||||
console.log(allUserUpgrades)
|
||||
return upgrade.preReqs.every(preReq => allUserUpgrades.includes(preReq))
|
||||
}
|
||||
|
||||
const quackStoreText = user =>
|
||||
Object.entries(quackStore)
|
||||
.filter(hasPreReqs(user))
|
||||
.map(quackStoreListing)
|
||||
.join('\n\n')
|
||||
const owns = (user, [name, upgrade]) => allUserQuackUpgrades(user).includes(name)
|
||||
|
||||
const quackStoreRoute = async ({ event, say, words }) => {
|
||||
const user = getUser(event.user)
|
||||
const ownedQuackItems = user => Object.entries(quackStore).filter(upgrade => owns(user, upgrade))
|
||||
|
||||
const unownedQuackItems = user => Object.entries(quackStore).filter(upgrade => !owns(user, upgrade))
|
||||
|
||||
const quackStoreText = user =>
|
||||
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 ??= {}
|
||||
const quacks = user.quacks ??= 0
|
||||
if (!words[1]) {
|
||||
if (!args[0] || !YEET) {
|
||||
await say(quackStoreText(user))
|
||||
return
|
||||
}
|
||||
const quackItem = quackStore[words[1]]
|
||||
if (!quackItem) {
|
||||
await say(`'${words[1]}' is not available in the quack store!`)
|
||||
const quackItem = quackStore[args[0]]
|
||||
if (!quackItem || !unownedQuackItems(user).find(([name]) => name === args[0])) {
|
||||
await say(`'${args[0]}' is not available in the quack store!`)
|
||||
return
|
||||
}
|
||||
const quacks = user.quacks ??= 0
|
||||
if (quackItem.cost > quacks) {
|
||||
await say(`${words[1]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
|
||||
await say(`${args[0]} costs ${quackItem.cost} Quacks, but you only have ${quacks}!`)
|
||||
return
|
||||
}
|
||||
user.quacks -= quackItem.cost
|
||||
user.quackUpgrades[quackItem.type] ??= []
|
||||
user.quackUpgrades[quackItem.type].push(words[1])
|
||||
saveGame()
|
||||
user.quackUpgrades[quackItem.type].push(args[0])
|
||||
if (quackItem.type === 'starter') {
|
||||
quackItem.effect(user)
|
||||
}
|
||||
await say(`You bought ${args[0]}!`)
|
||||
}
|
||||
|
||||
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 = {
|
||||
quackStoreRoute,
|
||||
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 = {
|
||||
ascent: {
|
||||
name: 'Ascent',
|
||||
|
@ -11,10 +18,159 @@ const quackStore = {
|
|||
name: 'Nuclear Fuel',
|
||||
type: 'cps',
|
||||
emoji: 'atom_symbol',
|
||||
description: 'The future is now. Boosts all CPS by 20%.',
|
||||
description: 'The future is now, old man. Boosts all CPS by 20%.',
|
||||
preReqs: ['ascent'],
|
||||
effect: cps => cps * 1.2,
|
||||
cost: 5
|
||||
},
|
||||
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,
|
||||
description,
|
||||
condition: user => user.items[type] >= count,
|
||||
condition: (user, squadGrades) => user.items[type] >= count && extraCondition(user, squadGrades),
|
||||
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 = {
|
||||
doubleClick: basic({
|
||||
name: 'Double-Click',
|
||||
type: 'mouse',
|
||||
description: 'Doubles the power of mice',
|
||||
count: 1,
|
||||
cost: 1_000,
|
||||
cost: 1_000
|
||||
}),
|
||||
stinkierCheese: basic({
|
||||
name: 'Stinkier Cheese',
|
||||
type: 'mouse',
|
||||
description: 'Mice are doubly motivated to hunt down HVAC Coins',
|
||||
count: 10,
|
||||
cost: 21_000,
|
||||
cost: 21_000
|
||||
}),
|
||||
biggerTeeth: basic({
|
||||
name: 'Bigger Teeth',
|
||||
type: 'mouse',
|
||||
description: 'Mice can intimidate twice as much HVAC out of their victims.',
|
||||
count: 25,
|
||||
cost: 50_000,
|
||||
cost: 50_000
|
||||
}),
|
||||
rats: evil({
|
||||
name: 'Rats',
|
||||
type: 'mouse',
|
||||
description: 'Consume the rotten remains of your foes',
|
||||
cost: 150_000,
|
||||
}),
|
||||
hoodedMice: heavenly({
|
||||
name: 'Hooded Mice',
|
||||
type: 'mouse',
|
||||
description: 'These monks have nearly reached enlightenment. 10x Mouse CPS.',
|
||||
cost: 1_000_000,
|
||||
multiplier: 10,
|
||||
}),
|
||||
babyMouse: baby({
|
||||
name: 'Baby Mouse',
|
||||
type: 'mouse',
|
||||
description: 'Squeak!',
|
||||
cost: 6_000_000,
|
||||
}),
|
||||
|
||||
fasterComputers: basic({
|
||||
name: 'Faster Computers',
|
||||
type: 'accountant',
|
||||
description: 'Accountants can ~steal~ optimize twice as much HVAC!',
|
||||
count: 1,
|
||||
cost: 11_000,
|
||||
}),
|
||||
lackOfMorality: basic({
|
||||
name: 'Lack of Morality',
|
||||
type: 'accountant',
|
||||
description: 'Accountants are taking a hint from nearby CEOs.',
|
||||
count: 10,
|
||||
cost: 200_000,
|
||||
}),
|
||||
widerBrains: basic({
|
||||
name: 'Wider Brains',
|
||||
type: 'accountant',
|
||||
description: 'For accountant do double of thinking.',
|
||||
count: 25,
|
||||
cost: 550_000,
|
||||
}),
|
||||
vastLayoffs: evil({
|
||||
name: 'Vast Layoffs',
|
||||
type: 'accountant',
|
||||
description: 'The weak are not part of our future.',
|
||||
cost: 2_450_000,
|
||||
}),
|
||||
charityFund: heavenly({
|
||||
name: 'Charity Fund',
|
||||
type: 'accountant',
|
||||
description: 'THIS one is more than just a tax break. 9x Accountant CPS.',
|
||||
cost: 16_333_333,
|
||||
multiplier: 9,
|
||||
}),
|
||||
mathBaby: baby({
|
||||
name: 'Math Baby',
|
||||
type: 'accountant',
|
||||
description: '2 + 2 = WAAH!',
|
||||
cost: 99_999_999,
|
||||
}),
|
||||
|
||||
biggerBlowhole: basic({
|
||||
name: 'Bigger Blowhole',
|
||||
type: 'whale',
|
||||
description: 'With all that extra air, whales have double power!',
|
||||
count: 1,
|
||||
cost: 120_000,
|
||||
cost: 120_000
|
||||
}),
|
||||
sassyWhales: basic({
|
||||
name: 'Sassy Whales',
|
||||
type: 'whale',
|
||||
description: 'These are the kind of whales that know how to get twice as much done',
|
||||
count: 10,
|
||||
cost: 3_000_000,
|
||||
cost: 3_000_000
|
||||
}),
|
||||
thinnerWater: basic({
|
||||
name: 'Thinner Water',
|
||||
type: 'whale',
|
||||
description: 'Whales can move twice as quickly through this physics-defying liquid',
|
||||
count: 25,
|
||||
cost: 6_000_000,
|
||||
cost: 6_000_000
|
||||
}),
|
||||
blightWhales: evil({
|
||||
name: 'Blight Whales',
|
||||
type: 'whale',
|
||||
description: `Infectious with evil, they swim the ocean spreading their spores.`,
|
||||
cost: 24_000_000
|
||||
}),
|
||||
whaleChoir: heavenly({
|
||||
name: 'Whale Choir',
|
||||
type: 'whale',
|
||||
description: `Their cleansing songs reverberate through the sea. 8x Whale CPS.`,
|
||||
cost: 144_000_000,
|
||||
multiplier: 8,
|
||||
}),
|
||||
smolWhales: baby({
|
||||
name: 'Smol Whales',
|
||||
type: 'whale',
|
||||
description: ``,
|
||||
cost: 8_400_000_000
|
||||
}),
|
||||
|
||||
greasyTracks: basic({
|
||||
name: 'Greasy Tracks',
|
||||
type: 'train',
|
||||
description: 'Lets trains deliver HVAC twice as efficiently',
|
||||
count: 1,
|
||||
cost: 1_300_000,
|
||||
cost: 1_300_000
|
||||
}),
|
||||
rocketThrusters: basic({
|
||||
name: 'Rocket Thrusters',
|
||||
type: 'train',
|
||||
description: 'That\'ll put some quack on your track',
|
||||
count: 10,
|
||||
cost: 22_000_000,
|
||||
cost: 22_000_000
|
||||
}),
|
||||
loudConductors: basic({
|
||||
name: 'Loud Conductors',
|
||||
type: 'train',
|
||||
description: 'Conductors can onboard twice as much HVAC',
|
||||
count: 25,
|
||||
cost: 65_000_000,
|
||||
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({
|
||||
name: 'Gasoline Fire',
|
||||
type: 'fire',
|
||||
description: 'Extremely good for breathing in.',
|
||||
count: 1,
|
||||
cost: 14_000_000,
|
||||
cost: 14_000_000
|
||||
}),
|
||||
extremelyDryFuel: basic({
|
||||
name: 'Extremely Dry Fuel',
|
||||
type: 'fire',
|
||||
description: 'Use the ignite command for a secret achievement.',
|
||||
description: 'Hey, psst, hey. Use the ignite command for a secret achievement.',
|
||||
count: 10,
|
||||
cost: 163_000_000,
|
||||
cost: 163_000_000
|
||||
}),
|
||||
cavemanFire: basic({
|
||||
name: 'Caveman Fire',
|
||||
type: 'fire',
|
||||
description: 'They just don\'t make \'em like they used to.',
|
||||
count: 25,
|
||||
cost: 700_000_000,
|
||||
cost: 700_000_000
|
||||
}),
|
||||
lava: evil({
|
||||
name: 'Lava',
|
||||
type: 'fire',
|
||||
description: `Hopefully no usurpers have any "accidents".`,
|
||||
cost: 4_200_000_000
|
||||
}),
|
||||
blueFire: heavenly({
|
||||
name: 'Blue Fire',
|
||||
type: 'fire',
|
||||
description: `You can hear it singing with delight. 7x Fire CPS.`,
|
||||
multiplier: 7,
|
||||
cost: 25_200_000_000
|
||||
}),
|
||||
cuteFire: baby({
|
||||
name: 'Cute Fire',
|
||||
type: 'fire',
|
||||
description: `I just met my perfect match...`,
|
||||
cost: 150_000_000_000
|
||||
}),
|
||||
|
||||
spoonerang: basic({
|
||||
name: 'Spoonerang',
|
||||
type: 'boomerang',
|
||||
description: 'Scoops up HVAC mid-flight',
|
||||
count: 1,
|
||||
cost: 200_000_000,
|
||||
cost: 200_000_000
|
||||
}),
|
||||
boomerAng: basic({
|
||||
name: 'Boomer-ang',
|
||||
type: 'boomerang',
|
||||
description: 'It\'s... old.',
|
||||
count: 10,
|
||||
cost: 1_200_000_000,
|
||||
cost: 1_200_000_000
|
||||
}),
|
||||
doubleRang: basic({
|
||||
name: 'Double-rang',
|
||||
type: 'boomerang',
|
||||
description: 'You throw one, but somehow catch two',
|
||||
count: 25,
|
||||
cost: 10_000_000_000,
|
||||
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({
|
||||
name: 'Lunar Power',
|
||||
type: 'moon',
|
||||
description: 'Out with the sol, in with the lun!',
|
||||
count: 1,
|
||||
cost: 3_300_000_000,
|
||||
cost: 3_300_000_000
|
||||
}),
|
||||
womanOnTheMoon: basic({
|
||||
name: 'Woman on the Moon',
|
||||
type: 'moon',
|
||||
description: 'There\'s no reason for it not to be a woman!',
|
||||
count: 10,
|
||||
cost: 39_700_000_000,
|
||||
cost: 39_700_000_000
|
||||
}),
|
||||
doubleCraters: basic({
|
||||
name: 'Double-Craters',
|
||||
type: 'moon',
|
||||
description: 'Making every side look like the dark side.',
|
||||
count: 25,
|
||||
cost: 165_000_000_000,
|
||||
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({
|
||||
name: 'Glass Butterfly',
|
||||
type: 'butterfly',
|
||||
description: 'Not your grandma\'s universe manipulation.',
|
||||
count: 1,
|
||||
cost: 51_000_000_000,
|
||||
cost: 51_000_000_000
|
||||
}),
|
||||
monarchMigration: basic({
|
||||
name: 'Monarch Migration',
|
||||
type: 'butterfly',
|
||||
description: 'This upgrade brought to you by milkweed.',
|
||||
count: 10,
|
||||
cost: 870_000_000_000,
|
||||
cost: 870_000_000_000
|
||||
}),
|
||||
quadWing: basic({
|
||||
name: 'Quad-Wing',
|
||||
type: 'butterfly',
|
||||
description: 'Sounds a lot like a trillion bees buzzing inside your head.',
|
||||
count: 25,
|
||||
cost: 2_550_000_000_000,
|
||||
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({
|
||||
name: 'Silver Mirror',
|
||||
type: 'mirror',
|
||||
description: 'Excellent for stabbing vampires.',
|
||||
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({
|
||||
name: 'Window',
|
||||
type: 'mirror',
|
||||
description: 'Only through looking around you can you acquire the self reflection necessary to control the thermostat.',
|
||||
count: 25,
|
||||
cost: 37_500_000_000_000,
|
||||
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({
|
||||
name: 'F-Zero',
|
||||
type: 'quade',
|
||||
description: 'Brings out his competitive spirit.',
|
||||
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({
|
||||
name: 'Adam',
|
||||
type: 'quade',
|
||||
description: 'He could probably reach the thermostat if he wanted.',
|
||||
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({
|
||||
name: 'Latest Node',
|
||||
type: 'hvacker',
|
||||
description: 'The old one has terrible ergonomics, tsk tsk.',
|
||||
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({
|
||||
name: 'Git Commits',
|
||||
type: 'hvacker',
|
||||
description: 'The heads of multiple people in a company are better than, for example, merely one head.',
|
||||
count: 25,
|
||||
cost: 7_000_000_000_000_000,
|
||||
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: {
|
||||
name: 'Homage',
|
||||
type: 'general',
|
||||
description: 'The power of original ideas increases your overall CPS by 10%',
|
||||
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 200,
|
||||
emoji: 'cookie',
|
||||
cost: 10_000_000_000,
|
||||
effect: (itemCps, user) => Math.ceil(itemCps * 1.1)
|
||||
effect: (itemCps, user) => itemCps * 1.1
|
||||
},
|
||||
iLoveHvac: {
|
||||
name: 'iLoveHvac',
|
||||
type: 'general',
|
||||
description: 'The power of love increases your overall CPS by 10%',
|
||||
condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
|
||||
emoji: 'heart',
|
||||
cost: 100_000_000_000_000,
|
||||
effect: (itemCps, user) => Math.ceil(itemCps * 1.1)
|
||||
}
|
||||
|
||||
// moreUpgrades: {
|
||||
// type: 'general',
|
||||
// description: 'Adds additional upgrades',
|
||||
// condition: user => Object.entries(user.items).reduce((total, [, countOwned]) => countOwned + total, 0) >= 400,
|
||||
// emoji: 'cookie',
|
||||
// cost: 10_000_000_000_000,
|
||||
// effect: nothing
|
||||
// },
|
||||
effect: (itemCps, user) => itemCps * 1.1
|
||||
},
|
||||
|
||||
digitalPickaxe: {
|
||||
name: 'Digital Pickaxe',
|
||||
type: 'mining',
|
||||
description: 'Break coinful digirocks into bits, increasing the power of !mine',
|
||||
condition: user => user.interactions > 100,
|
||||
emoji: 'pick',
|
||||
cost: 100_000,
|
||||
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
|
||||
},
|
||||
vacuum: {
|
||||
name: 'Digital Vacuum',
|
||||
type: 'mining',
|
||||
description: 'Suck up leftover HVAC dust, greatly increasing the power of !mine',
|
||||
condition: user => user.interactions > 500,
|
||||
emoji: 'vacuum',
|
||||
cost: 10_000_000,
|
||||
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
|
||||
},
|
||||
mineCart: {
|
||||
name: 'HVAC Mine Cart',
|
||||
type: 'mining',
|
||||
description: 'You\'d shine like a diamond, down in the !mine',
|
||||
condition: user => user.interactions > 1500,
|
||||
emoji: 'shopping_trolley',
|
||||
cost: 100_000_000_000,
|
||||
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
|
||||
},
|
||||
fpga: {
|
||||
name: 'FPGA Miner',
|
||||
type: 'mining',
|
||||
description: 'Wait, what kind of mining is this again?',
|
||||
condition: user => user.interactions > 5000,
|
||||
emoji: 'floppy_disk',
|
||||
cost: 1_000_000_000_000_000,
|
||||
effect: (mineTotal, user) => mineTotal + (getCPS(user) * 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
setUpgrades(module.exports)
|
||||
|
|
|
@ -1,38 +1,80 @@
|
|||
const fs = require('fs')
|
||||
const jokes = require('../jokes')
|
||||
const achievements = require('./achievements');
|
||||
const buyableItems = require("./buyableItems");
|
||||
const upgrades = require("./upgrades");
|
||||
const quackStore = require("./quackstore");
|
||||
//const jokes = require('../jokes')
|
||||
const achievements = require('./achievements')
|
||||
const buyableItems = require('./buyableItems')
|
||||
const { quackStore, getChaos } = require('./quackstore')
|
||||
|
||||
let upgrades
|
||||
const setUpgrades = upg => {
|
||||
upgrades = upg
|
||||
}
|
||||
|
||||
const saveFile = 'hvacoins.json'
|
||||
|
||||
const logError = msg => msg ? console.error(msg) : () => { /* Don't log empty message */ }
|
||||
const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ }
|
||||
|
||||
const loadGame = () => parseOr(fs.readFileSync('./' + saveFile, 'utf-8'),
|
||||
const loadGame = () => {
|
||||
const game = parseOr(fs.readFileSync('./' + saveFile, 'utf-8'),
|
||||
() => ({
|
||||
users: {},
|
||||
nfts: [],
|
||||
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 {
|
||||
if (typeof parseable === 'function') {
|
||||
parseable = parseable()
|
||||
}
|
||||
return JSON.parse(parseable)
|
||||
} catch (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
|
||||
const saveGame = () => {
|
||||
if (saves % 100 === 0) {
|
||||
fs.writeFileSync('./backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_'), JSON.stringify(game))
|
||||
const saveGame = (after, force = true) => {
|
||||
if (saves % 20 === 0) {
|
||||
makeBackup()
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
const maybeNews = say => {
|
||||
const random = Math.random()
|
||||
|
@ -47,14 +89,116 @@ const maybeNews = say => {
|
|||
|
||||
const idFromWord = word => {
|
||||
if (!word?.startsWith('<@') || !word.endsWith('>')) {
|
||||
return null
|
||||
}
|
||||
return getIdFromName(word)
|
||||
} else {
|
||||
return word.substring(2, word.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const getSeconds = () => new Date().getTime() / 1000
|
||||
|
||||
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 { users, nfts, squad } = game
|
||||
|
@ -76,50 +220,60 @@ const addAchievement = (user, achievementName, say) => {
|
|||
}
|
||||
setTimeout(async () => {
|
||||
user.achievements[achievementName] = true
|
||||
saveGame()
|
||||
saveGame(`${user.name} earned ${achievementName}`)
|
||||
await say(`You earned the achievement ${achievements[achievementName].name}!`)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const getUser = userId => {
|
||||
if (!users[userId]) {
|
||||
users[userId] = {
|
||||
coins: 0,
|
||||
items: {},
|
||||
upgrades: {},
|
||||
achievements: {},
|
||||
coinsAllTime: 0,
|
||||
prestige: 0
|
||||
const fuzzyMatcher = string => new RegExp((string?.toLowerCase() || '').split('').join('.*'), 'i')
|
||||
|
||||
let knownUsers = {}
|
||||
const getIdFromName = name => {
|
||||
const matcher = fuzzyMatcher(name?.toLowerCase())
|
||||
const found = Object.entries(knownUsers).find(([id, knownName]) => matcher.test(knownName?.toLowerCase()))
|
||||
if (found) {
|
||||
return found[0]
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getUser = (userId, updateCoins = false) => {
|
||||
users[userId] ??= {}
|
||||
users[userId].coins ??= 0
|
||||
users[userId].items ??= {}
|
||||
users[userId].upgrades ??= {}
|
||||
users[userId].achievements ??= {}
|
||||
users[userId].coinsAllTime ??= users[userId].coins
|
||||
users[userId].prestige ??= 0
|
||||
users[userId].startDate ??= new Date()
|
||||
if (updateCoins) {
|
||||
users[userId].coins = getCoins(userId)
|
||||
}
|
||||
return users[userId]
|
||||
}
|
||||
|
||||
const addCoins = (user, add) => {
|
||||
user.coins += add
|
||||
user.coinsAllTime += add
|
||||
user.coinsAllTime = Math.floor(user.coinsAllTime)
|
||||
user.coins = Math.floor(user.coins)
|
||||
}
|
||||
|
||||
const getCoins = userId => {
|
||||
const user = getUser(userId)
|
||||
const currentTime = getSeconds()
|
||||
const lastCheck = user.lastCheck || currentTime
|
||||
const secondsPassed = currentTime - lastCheck
|
||||
|
||||
const increase = getCPS(userId) * secondsPassed
|
||||
user.coins += increase
|
||||
user.coinsAllTime += increase
|
||||
user.coins = Math.floor(user.coins)
|
||||
addCoins(user, getCPS(user) * secondsPassed)
|
||||
|
||||
user.lastCheck = currentTime
|
||||
setHighestCoins(userId)
|
||||
saveGame()
|
||||
//saveGame()
|
||||
return user.coins
|
||||
}
|
||||
|
||||
const getCPS = userId => {
|
||||
const user = getUser(userId)
|
||||
const getCPS = user => {
|
||||
const userItems = user?.items || {}
|
||||
return Math.round(Object.keys(userItems).reduce((total, itemName) => total + getItemCps(user, itemName), 0))
|
||||
}
|
||||
|
@ -130,16 +284,31 @@ const squadUpgrades = {
|
|||
tastyKeyboards: {
|
||||
name: 'Tasty Keyboards',
|
||||
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,
|
||||
emoji: 'keyboard'
|
||||
},
|
||||
copyPasteMacro: {
|
||||
name: 'Copy-Paste Macro.',
|
||||
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,
|
||||
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)
|
||||
.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 baseCps = buyableItems[itemName].earning
|
||||
// console.log('')
|
||||
// console.log(`${itemName} CPS:`)
|
||||
// console.log('baseCps', baseCps)
|
||||
|
||||
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 || []
|
||||
const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), itemUpgradeCps)
|
||||
user.upgrades.general ??= []
|
||||
const userGeneralUpgrades = user.upgrades.general
|
||||
const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), 1)
|
||||
// console.log('generalUpgradeCps', generalUpgradeCps)
|
||||
|
||||
const achievementCount = Object.keys(user.achievements || {}).length
|
||||
const achievementMultiplier = Math.pow(1.01, achievementCount)
|
||||
// console.log('achievementMultiplier', achievementMultiplier)
|
||||
|
||||
const userQuackgrades = user.quackUpgrades?.cps || []
|
||||
const quackMultiplier = userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1)
|
||||
const quackGrade = quackGradeMultiplier(user)
|
||||
// console.log('quackgrade', quackGrade)
|
||||
|
||||
const prestigeMultiplier = 1 + ((user.prestige || 0) * 0.01)
|
||||
const pMult = prestigeMultiplier(user)
|
||||
// console.log('prestigeMultiplier', pMult)
|
||||
|
||||
return achievementMultiplier *
|
||||
quackMultiplier *
|
||||
prestigeMultiplier *
|
||||
getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), generalUpgradeCps)
|
||||
const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1)
|
||||
// console.log('squadGradeMultiplier', squadGradeMultiplier)
|
||||
|
||||
const petMultiplier = petQuackGradeMultiplier(user)
|
||||
//console.log('petMultiplier', petMultiplier)
|
||||
|
||||
const total =
|
||||
baseCps *
|
||||
achievementMultiplier *
|
||||
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 = {
|
||||
saveGame,
|
||||
makeBackup,
|
||||
logError,
|
||||
parseOr,
|
||||
maybeNews,
|
||||
|
@ -190,5 +562,25 @@ module.exports = {
|
|||
getItemCps,
|
||||
squadUpgrades,
|
||||
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 base64 = require('base-64')
|
||||
const slack = require('../../slack')
|
||||
const { game: { users } } = require('./utils')
|
||||
const { game: { users }, getUser, fuzzyMatcher } = require('./utils')
|
||||
|
||||
const apiGetUserId = hash => {
|
||||
return Object.entries(userGetter.users)
|
||||
return Object.entries(users)
|
||||
.filter(([id, user]) => user.pwHash === hash)
|
||||
.map(([id, user]) => id)[0]
|
||||
}
|
||||
|
@ -17,12 +17,21 @@ const makeHash = pw =>
|
|||
.update(pw)
|
||||
.digest('hex')
|
||||
|
||||
const illegalCommands = ['!', '!b']
|
||||
const lastCalls = {}
|
||||
const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
|
||||
if (illegalCommands.find(command => commandNames.includes(command))) {
|
||||
commandNames.forEach(name =>
|
||||
app.get('/' + name.replace(/!/gi, ''), async (req, res) => res.send('Command is illegal over the web api.'))
|
||||
)
|
||||
return
|
||||
}
|
||||
const route = async (req, res) => {
|
||||
const say = async msg => res.send(msg)
|
||||
const say = async msg => res.send(msg + '\n')
|
||||
try {
|
||||
const words = ['', ...Object.keys(req.query)]
|
||||
console.log('INCOMING API CALL:', name, words)
|
||||
const [commandName, ...args] = words
|
||||
console.log('INCOMING API CALL:', commandName, words)
|
||||
const encoded = req.header('Authorization').substring(5)
|
||||
const decoded = base64.decode(encoded).substring(1)
|
||||
const event = {
|
||||
|
@ -37,8 +46,8 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
|
|||
console.log(' bad password')
|
||||
return
|
||||
}
|
||||
const lastCall = userGetter.users[event.user].lastApiCall || 0
|
||||
const secondsBetweenCalls = 5
|
||||
const lastCall = lastCalls[event.user] || 0
|
||||
const secondsBetweenCalls = 30
|
||||
const currentTime = Math.floor(new Date().getTime() / 1000)
|
||||
if (lastCall + secondsBetweenCalls > currentTime) {
|
||||
res.status(400)
|
||||
|
@ -46,13 +55,22 @@ const addCommand = ({ commandNames, helpText, action, condition, hidden }) => {
|
|||
console.log(' rate limited')
|
||||
return
|
||||
}
|
||||
console.log(` went through for ${slack.ourUsers[event.user]}`)
|
||||
userGetter.users[event.user].lastApiCall = currentTime
|
||||
console.log(` went through for ${slack.users[event.user]}`)
|
||||
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) {
|
||||
console.error(e)
|
||||
await say(e.stack)
|
||||
console.error('route error', e)
|
||||
await say(`Routing error. Make sure you've set up API access with the !setpw command in slack!\n` +
|
||||
'Then you can use calls like `curl -u ":yourpw" \'http://10.3.0.48:3001/stonks\'`')
|
||||
}
|
||||
}
|
||||
commandNames.forEach(name =>
|
||||
|
|
|
@ -1,43 +1,63 @@
|
|||
const slack = require('../slack')
|
||||
const { updateAll } = require('../games/hvacoins/utils')
|
||||
|
||||
const tie = 'TIE'
|
||||
|
||||
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) =>
|
||||
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' +
|
||||
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2, channelMap }) =>
|
||||
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2], channelMap) + '\n' +
|
||||
'```' + textFromBoard(board) + '\n```'
|
||||
|
||||
const addChoiceEmojis = async ({ choices, channel, ts }) => {
|
||||
const addEmoji = async emojiName =>
|
||||
const addEmoji = async emojiName => {
|
||||
try {
|
||||
await slack.app.client.reactions.add({
|
||||
channel,
|
||||
timestamp: ts,
|
||||
name: emojiName
|
||||
})
|
||||
} catch (ignore) {
|
||||
}
|
||||
}
|
||||
for (const choice of choices) {
|
||||
await addEmoji(choice)
|
||||
}
|
||||
}
|
||||
|
||||
const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => {
|
||||
if (event.channel_type === 'im') {
|
||||
if (event.channel_type !== 'im') {
|
||||
return;
|
||||
}
|
||||
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 msg = messageFromBoard({
|
||||
console.log('Messaging opponent ' + slack.users[opponent.substring(2, opponent.length - 1)])
|
||||
const channelMap = {}
|
||||
const msg = () => messageFromBoard({
|
||||
dataName,
|
||||
gameName,
|
||||
textFromBoard,
|
||||
board: initialBoard(),
|
||||
player1: '<@' + event.user + '>',
|
||||
player2: opponent
|
||||
player2: opponent,
|
||||
channelMap
|
||||
})
|
||||
const sent = await say(msg)
|
||||
await addChoiceEmojis({ ...sent, choices: turnChoiceEmojis })
|
||||
const sent = await say(msg())
|
||||
channelMap[event.user] = {
|
||||
channel: sent.channel,
|
||||
ts: sent.ts
|
||||
}
|
||||
await updateAll({ name: gameName, text: msg(), add: sent })
|
||||
await addChoiceEmojis({...sent, choices: turnChoiceEmojis})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, { board, players })
|
||||
const encodeGame = (dataKey, board, players, channelMap = {}) => slack.encodeData(dataKey, { board, players, channelMap })
|
||||
|
||||
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
|
||||
|
||||
|
@ -50,7 +70,8 @@ const getMessages = winner => {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -61,12 +82,15 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
|
|||
|
||||
const game = decodeGame(dataName, message.messages[0].text)
|
||||
if (!game) {
|
||||
console.log('could not decode game')
|
||||
return
|
||||
}
|
||||
|
||||
const { board, players } = game
|
||||
game.channelMap ??= {}
|
||||
const { board, players, channelMap } = game
|
||||
let winner = checkWinner(board)
|
||||
if (winner) {
|
||||
console.log('winner found: ' + winner)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -80,32 +104,58 @@ const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turn
|
|||
}
|
||||
winner = checkWinner(board)
|
||||
|
||||
const boardMessage = messageFromBoard({
|
||||
const boardMessage = () => messageFromBoard({
|
||||
dataName,
|
||||
gameName,
|
||||
textFromBoard,
|
||||
board,
|
||||
player1,
|
||||
player2
|
||||
player2,
|
||||
channelMap
|
||||
})
|
||||
const winnerMessages = getMessages(winner)
|
||||
await say(boardMessage + winnerMessages.you)
|
||||
if (!winner) {
|
||||
await say('Waiting for opponent\'s response...')
|
||||
}
|
||||
|
||||
const removeEmoji = async emojiName =>
|
||||
if (winner) {
|
||||
await updateAll({ name: gameName, text: boardMessage() + '\nSomebody won! I do not yet know who!' })
|
||||
const removeEmoji = emojiName => Object.values(channelMap).forEach(({ channel, ts }) =>
|
||||
slack.app.client.reactions.remove({
|
||||
channel: event.item.channel,
|
||||
timestamp: message.messages[0]?.ts,
|
||||
channel,
|
||||
timestamp: ts,
|
||||
name: emojiName
|
||||
})
|
||||
}))
|
||||
turnChoiceEmojis.forEach(removeEmoji)
|
||||
return
|
||||
}
|
||||
const winnerMessages = getMessages(winner)
|
||||
// await say(boardMessage() + winnerMessages.you)
|
||||
console.log('TurnHandler', { gameName, boardMessage: boardMessage() })
|
||||
// await updateAll({ name: gameName, text: boardMessage() + '\nTurnHandler' })
|
||||
// if (!winner) {
|
||||
// await say('Waiting for opponent\'s response...')
|
||||
// }
|
||||
|
||||
// const removeEmoji = async emojiName =>
|
||||
// slack.app.client.reactions.remove({
|
||||
// channel: event.item.channel,
|
||||
// timestamp: message.messages[0]?.ts,
|
||||
// name: emojiName
|
||||
// })
|
||||
// turnChoiceEmojis.forEach(removeEmoji)
|
||||
console.log('SENDING to ' + opponent)
|
||||
if (!channelMap[opponent]) {
|
||||
const sentBoard = await slack.app.client.chat.postMessage({
|
||||
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) {
|
||||
const sentBoard = channelMap[opponent]
|
||||
await addChoiceEmojis({ ...sentBoard, choices: turnChoiceEmojis })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
const routine = require("./routine");
|
||||
const routine = require('./routine')
|
||||
|
||||
const emptyBoard = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
|
||||
|
||||
const textFromBoard = board =>
|
||||
` ${board[0]} | ${board[1]} | ${board[2]} \n` +
|
||||
`-----------\n` +
|
||||
'-----------\n' +
|
||||
` ${board[3]} | ${board[4]} | ${board[5]} \n` +
|
||||
`-----------\n` +
|
||||
'-----------\n' +
|
||||
` ${board[6]} | ${board[7]} | ${board[8]}`
|
||||
|
||||
const numEmojis = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
|
||||
|
@ -21,7 +21,7 @@ const winningThrees = [
|
|||
[2, 5, 8],
|
||||
|
||||
[0, 4, 8],
|
||||
[2, 4, 6],
|
||||
[2, 4, 6]
|
||||
]
|
||||
|
||||
const checkWinner = board => {
|
||||
|
@ -71,4 +71,3 @@ routine.build({
|
|||
makeMove: applyTurn,
|
||||
checkWinner
|
||||
})
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ const getTrivia = async () => axios.get('https://opentdb.com/api.php?amount=10&c
|
|||
}
|
||||
})
|
||||
.then(res => res.data.results)
|
||||
.catch(console.error)
|
||||
.catch(e => console.error('trivia error', e))
|
||||
|
||||
module.exports = {
|
||||
getTrivia
|
||||
|
|
10
src/index.js
10
src/index.js
|
@ -26,12 +26,12 @@ onTempChangeRequested(change => {
|
|||
case 'Hotter': {
|
||||
lowTemp += 2
|
||||
highTemp += 2
|
||||
break;
|
||||
break
|
||||
}
|
||||
case 'Colder': {
|
||||
lowTemp -= 2
|
||||
highTemp -= 2
|
||||
break;
|
||||
break
|
||||
}
|
||||
case 'Good': {
|
||||
return
|
||||
|
@ -42,9 +42,9 @@ onTempChangeRequested(change => {
|
|||
lowTemp = cleanTemp(lowTemp)
|
||||
|
||||
const mode =
|
||||
indoorTemperature < lowTemp ? heatMode : // Heat if lower than low
|
||||
indoorTemperature > highTemp ? coolMode : // Cool if hotter than high
|
||||
change === 'Hotter' ? heatMode : coolMode // Otherwise (lower priority) follow the requested change
|
||||
indoorTemperature < lowTemp ? heatMode // Heat if lower than low
|
||||
: indoorTemperature > highTemp ? coolMode // Cool if hotter than high
|
||||
: change === 'Hotter' ? heatMode : coolMode // Otherwise (lower priority) follow the requested change
|
||||
|
||||
if (!mode) {
|
||||
return
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
const { App: SlackApp } = require('@slack/bolt')
|
||||
const config = require('../config')
|
||||
const fs = require('fs')
|
||||
const { addReactions, saveGame, setSlackAppClientChatUpdate, parseOr } = require('../games/hvacoins/utils')
|
||||
|
||||
const temperatureChannelId = 'C034156CE03'
|
||||
const hvackerBotUserId = 'U0344TFA7HQ'
|
||||
const sageUserId = 'U028BMEBWBV'
|
||||
const dailyStandupChannelId = 'C03L533AU3Z'
|
||||
|
||||
const pollingMinutes = 5
|
||||
const pollingPeriod = 1000 * 60 * pollingMinutes
|
||||
|
||||
const MAX_POLLS = 3
|
||||
const HOURS_PER_WINDOW = 2
|
||||
|
||||
const colderEmoji = 'snowflake'
|
||||
const hotterEmoji = 'fire'
|
||||
const goodEmoji = '+1'
|
||||
|
||||
let app
|
||||
try {
|
||||
app = new SlackApp({
|
||||
const app = new SlackApp({
|
||||
token: config.slackBotToken,
|
||||
signingSecret: config.slackSigningSecret,
|
||||
appToken: config.slackAppToken,
|
||||
|
@ -24,11 +26,8 @@ try {
|
|||
// temperatureChannelId = fetched.channels.filter(channel => channel.name === 'thermo-posting')[0].id
|
||||
// console.log('techThermostatChannelId', temperatureChannelId)
|
||||
// })
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize SlackApp', e)
|
||||
}
|
||||
|
||||
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold']
|
||||
const pollTriggers = ['!temp', '!temperature', '!imhot', '!imcold', '!imfreezing', '!idonthavemysweater']
|
||||
const halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
|
||||
|
||||
const sendHelp = async (say, prefix) => {
|
||||
|
@ -41,7 +40,8 @@ const sendHelp = async (say, prefix) => {
|
|||
await say({
|
||||
text: prefix +
|
||||
`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 }) => {
|
||||
console.log('reaction_added', event)
|
||||
for (const listener of reactionListeners) {
|
||||
listener({ event, say })
|
||||
}
|
||||
})
|
||||
|
||||
const ourUsers = {
|
||||
U028BMEBWBV: 'Sage',
|
||||
U02U15RFK4Y: 'Adam',
|
||||
U02AAB54V34: 'Houston',
|
||||
U02KYLVK1GV: 'Quade',
|
||||
U017PG4EL1Y: 'Max',
|
||||
UTDLFGZA5: 'Tyler',
|
||||
U017CB5L1K3: 'Andres'
|
||||
const users = parseOr(fs.readFileSync('./users.json', 'utf-8'),
|
||||
() => ({}))
|
||||
|
||||
const buildSayPrepend = ({ say, prepend }) => async msg => {
|
||||
if (typeof(msg) === 'string') {
|
||||
return say(prepend + msg)
|
||||
}
|
||||
return say({
|
||||
...msg,
|
||||
text: prepend + msg.text
|
||||
})
|
||||
}
|
||||
|
||||
process.once('SIGINT', code => {
|
||||
saveGame('SIGINT', true)
|
||||
process.exit()
|
||||
})
|
||||
|
||||
let pollHistory = []
|
||||
const activePolls = {}
|
||||
const testId = 'U028BMEBWBV_TEST'
|
||||
let testMode = false
|
||||
app.event('message', async ({ event, context, client, say }) => {
|
||||
console.log(event)
|
||||
if (event.user === sageUserId) {
|
||||
if (event.subtype !== 'message_changed' && event?.text !== '!') {
|
||||
console.log('message.event', {
|
||||
...event,
|
||||
userName: users[event.user]
|
||||
})
|
||||
}
|
||||
if (event?.user === users.Admin) {
|
||||
if (event?.text.startsWith('!')) {
|
||||
if (testMode) {
|
||||
await messageSage('Currently in test mode!')
|
||||
await messageAdmin('Currently in test mode!')
|
||||
}
|
||||
}
|
||||
if (event?.text === '!test') {
|
||||
testMode = !testMode
|
||||
await messageSage(`TestMode: ${testMode} with ID ${testId}`)
|
||||
await messageAdmin(`TestMode: ${testMode} with ID ${testId}`)
|
||||
} else if (event?.text === '!notest') {
|
||||
testMode = false
|
||||
await messageSage(`TestMode: ${testMode}`)
|
||||
await messageAdmin(`TestMode: ${testMode}`)
|
||||
}
|
||||
if (testMode) {
|
||||
event.user = testId
|
||||
}
|
||||
// console.log(event.blocks[0].elements[0])
|
||||
}
|
||||
for (const listener of messageListeners) {
|
||||
listener({ event, say })
|
||||
}
|
||||
console.log('MSG', ourUsers[event.user], "'" + event.text + "'", new Date().toLocaleTimeString())
|
||||
if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') {
|
||||
if (event.user) {
|
||||
console.log('MSG', users[event.user], "'" + event.text + "'", new Date().toLocaleTimeString())
|
||||
}
|
||||
if (event.user === users.Admin && event.channel === 'D0347Q4H9FE') {
|
||||
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')) {
|
||||
await postToTechThermostatChannel(event.text.substring(4).trim())
|
||||
return
|
||||
}
|
||||
if (event.text?.startsWith('!saytoq ')) {
|
||||
await messageQuade(event.text.substring(7).trim())
|
||||
return
|
||||
}
|
||||
if (event.text?.startsWith('!saytos')) {
|
||||
await messageSage(event.text.substring(7).trim())
|
||||
await postToTechThermostatChannel(event.text.substring(4).trim().replace('@here', '<!here>'))
|
||||
return
|
||||
}
|
||||
}
|
||||
const eventText = event.text?.toLowerCase() || ''
|
||||
|
||||
if (eventText.startsWith('!help')) {
|
||||
if (eventText === '!help') {
|
||||
await sendHelp(say)
|
||||
return
|
||||
}
|
||||
|
||||
if (!pollTriggers.includes(eventText)) {
|
||||
if (!pollTriggers.includes(eventText) || event.user === users.John) {
|
||||
if (halfTriggers.includes(eventText)) {
|
||||
await sendHelp(say, 'It looks like you might want to change the temperature.')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.channel !== temperatureChannelId) {
|
||||
return say(`Please request polls in the appropriate channel.`)
|
||||
}
|
||||
|
||||
if (activePolls[event.channel]) {
|
||||
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
|
||||
return
|
||||
}
|
||||
const now = new Date()
|
||||
const windowStart = new Date()
|
||||
windowStart.setHours(now.getHours() - HOURS_PER_WINDOW)
|
||||
|
||||
const pollsInWindow = pollHistory.filter(pollTime => pollTime > windowStart)
|
||||
const pollText = MAX_POLLS === 1 ? 'poll' : 'polls'
|
||||
const hourText = HOURS_PER_WINDOW === 1 ? 'hour' : 'hours'
|
||||
|
||||
if (pollsInWindow.length >= MAX_POLLS) {
|
||||
await postToTechThermostatChannel({ text: `You have exceeded the limit of ${MAX_POLLS} ${pollText} per ${HOURS_PER_WINDOW} ${hourText}!` })
|
||||
return
|
||||
}
|
||||
|
||||
if (pollHistory.push(now) > MAX_POLLS) {
|
||||
[, ...pollHistory] = pollHistory
|
||||
}
|
||||
|
||||
activePolls[event.channel] = true
|
||||
const pollTs = await startPoll()
|
||||
|
@ -139,36 +176,73 @@ app.event('message', async ({ event, context, client, say }) => {
|
|||
timestamp: pollTs,
|
||||
full: true
|
||||
})
|
||||
|
||||
const reactPosters = {}
|
||||
reactions.message.reactions.forEach(r => r.users.forEach(user => {
|
||||
reactPosters[user] ??= []
|
||||
reactPosters[user].push(r.name)
|
||||
}))
|
||||
|
||||
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 hotterVotes = reactCounts[hotterEmoji]
|
||||
const colderVotes = reactCounts[colderEmoji]
|
||||
const contentVotes = reactCounts[goodEmoji] || 0
|
||||
let hotterVotes = reactCounts[hotterEmoji] || 0
|
||||
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) {
|
||||
text = `<@${users[users.ThermoController]}> The people have spoken, and would like to `
|
||||
text += 'raise the temperature, quack.'
|
||||
requestTempChange('Hotter')
|
||||
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
|
||||
text = `<@${users[users.ThermoController]}> The people have spoken, and would like to `
|
||||
text += 'lower the temperature, quack quack.'
|
||||
requestTempChange('Colder')
|
||||
} else {
|
||||
text = `The people have spoken, and would like to `
|
||||
text += 'keep the temperature as-is, quaaack.'
|
||||
requestTempChange('Good')
|
||||
}
|
||||
|
||||
await postToTechThermostatChannel({ text })
|
||||
delete activePolls[event.channel]
|
||||
if (pendingRestart && Object.entries(activePolls).length === 0) {
|
||||
await messageAdmin('Performing pending restart!')
|
||||
saveGame(null, true)
|
||||
process.exit(0)
|
||||
}
|
||||
}, pollingPeriod)
|
||||
})
|
||||
|
||||
let pendingRestart = false
|
||||
|
||||
;(async () => {
|
||||
await app.start().catch(console.error)
|
||||
await app.start()
|
||||
console.log('Slack Bolt has started')
|
||||
// setTimeout(async () => {
|
||||
// await messageSage('<https://i.imgur.com/VCvfvdz.png|...>')
|
||||
// }, 2000)
|
||||
})()
|
||||
|
||||
const postToTechThermostatChannel = async optionsOrText => {
|
||||
|
@ -180,8 +254,7 @@ const postToTechThermostatChannel = async optionsOrText => {
|
|||
return app.client.chat.postMessage({ ...optionsOrText, channel: temperatureChannelId })
|
||||
}
|
||||
|
||||
const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText)
|
||||
const messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText)
|
||||
const messageAdmin = async optionsOrText => messageIn(users.Admin, optionsOrText)
|
||||
|
||||
const messageIn = async (channel, optionsOrText) => {
|
||||
if (optionsOrText === null || typeof optionsOrText !== 'object') {
|
||||
|
@ -194,19 +267,16 @@ const messageIn = async (channel, optionsOrText) => {
|
|||
|
||||
const startPoll = async () => {
|
||||
const sent = await postToTechThermostatChannel({
|
||||
text: `<!here|here> Temperature poll requested! In ${pollingMinutes} minutes the temperature will be adjusted.\n` +
|
||||
`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!)`
|
||||
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.
|
||||
(Note that I can't actually change the temperature yet. Make ${users.ThermoController} do it!)`
|
||||
})
|
||||
const addReaction = async emojiName =>
|
||||
app.client.reactions.add({
|
||||
channel: temperatureChannelId,
|
||||
await addReactions({
|
||||
app,
|
||||
channelId: temperatureChannelId,
|
||||
timestamp: sent.ts,
|
||||
name: emojiName
|
||||
reactions: [colderEmoji, hotterEmoji, goodEmoji]
|
||||
})
|
||||
await addReaction(colderEmoji)
|
||||
await addReaction(hotterEmoji)
|
||||
await addReaction(goodEmoji)
|
||||
return sent.ts
|
||||
}
|
||||
|
||||
|
@ -218,10 +288,12 @@ const requestTempChange = change => {
|
|||
tempChangeListeners.forEach(listener => listener(change))
|
||||
}
|
||||
|
||||
// noinspection HttpUrlsUsage
|
||||
const encodeData = (key, data) =>
|
||||
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
|
||||
|
||||
const decodeData = (key, message) => {
|
||||
try {
|
||||
const regex = new RegExp(`http://${key}ZZZ[^|]*`)
|
||||
let match = message.match(regex)
|
||||
if (!match) {
|
||||
|
@ -229,25 +301,44 @@ const decodeData = (key, message) => {
|
|||
}
|
||||
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
|
||||
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const onReaction = listener => reactionListeners.push(listener)
|
||||
|
||||
const channelIsIm = async channel => (await app.client.conversations.info({ channel }))?.channel?.is_im
|
||||
|
||||
const wasMyMessage = async event => {
|
||||
const text = (await app.client.conversations.history({
|
||||
channel: event.item.channel,
|
||||
latest: event.item.ts,
|
||||
limit: 1,
|
||||
inclusive: true
|
||||
})).messages[0].text
|
||||
|
||||
const decoded = decodeData('commandPayload', text)
|
||||
return decoded.event.user === event.user
|
||||
}
|
||||
|
||||
onReaction(async ({ event }) => {
|
||||
if (event.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 {
|
||||
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setSlackAppClientChatUpdate(app.client.chat.update)
|
||||
|
||||
module.exports = {
|
||||
app,
|
||||
hvackerBotUserId,
|
||||
temperatureChannelId,
|
||||
dailyStandupChannelId,
|
||||
onAction: app.action,
|
||||
getMessage,
|
||||
updateMessage: app.client.chat.update,
|
||||
|
@ -257,10 +348,12 @@ module.exports = {
|
|||
onReaction,
|
||||
encodeData,
|
||||
decodeData,
|
||||
sageUserId,
|
||||
messageSage,
|
||||
messageAdmin,
|
||||
messageIn,
|
||||
testMode,
|
||||
testId,
|
||||
ourUsers
|
||||
users,
|
||||
buildSayPrepend,
|
||||
pollTriggers,
|
||||
pendingRestart
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue