2337 lines
73 KiB
JavaScript
2337 lines
73 KiB
JavaScript
const {
|
||
saveGame,
|
||
logError,
|
||
getCPS,
|
||
squadUpgrades,
|
||
getItemCps,
|
||
squadIsMissing,
|
||
idFromWord,
|
||
getCoins,
|
||
getUser,
|
||
getUserSync,
|
||
saveUser,
|
||
commas,
|
||
addAchievement,
|
||
shufflePercent,
|
||
parseAll,
|
||
getRandomFromArray,
|
||
chaosFilter,
|
||
addReactions,
|
||
removeReactions,
|
||
setHighestCoins,
|
||
definitelyShuffle,
|
||
getCompletedSquadgradeNames,
|
||
setKnownUsers,
|
||
dayOfYear,
|
||
daysSinceEpoch,
|
||
userHasCheckedQuackgrade,
|
||
fuzzyMatcher,
|
||
addCoins,
|
||
game,
|
||
updateAll,
|
||
logMemoryUsage
|
||
} = require('./utils')
|
||
const { nfts, squad, users, horrors, stonkMarket, pet } = game
|
||
const pets = require('./gotcha')
|
||
|
||
const exec = require('child_process').exec
|
||
const { createReadStream, createWriteStream, existsSync, readFileSync, writeFileSync } = require('fs')
|
||
const { readdir } = require('fs/promises')
|
||
|
||
const slack = require('../../slack')
|
||
const buyableItems = require('./buyableItems')
|
||
const upgrades = require('./upgrades')
|
||
const achievements = require('./achievements')
|
||
const webapi = require('./webapi')
|
||
const prestige = require('./prestige')
|
||
const lore = require('./lore')
|
||
const { getChaos, quackStore } = require('./quackstore')
|
||
const settings = require('./settings')
|
||
const https = require('https')
|
||
|
||
// const readline = require('readline').createInterface({
|
||
// input: process.stdin,
|
||
// output: process.stdout
|
||
// })
|
||
// const read = () => {
|
||
// readline.question(`What do YOU want? `, async want => {
|
||
// want && await slack.messageAdmin(want)
|
||
// read()
|
||
// })
|
||
// }
|
||
// ;(async () => {
|
||
// read()
|
||
// })();
|
||
|
||
setKnownUsers(slack.users)
|
||
|
||
const getUpgradeEmoji = upgrade => upgrade.emoji || buyableItems[upgrade.type].emoji
|
||
const upgradeText = (user, showOwned = false) => {
|
||
const userDoesNotHave = ([upgradeName, upgrade]) => hasUpgrade(user, upgrade, upgradeName) === showOwned
|
||
const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
|
||
const format = ([, value]) => `:${getUpgradeEmoji(value)}: *${value.name}* - ${commas(value.cost)}\n_${value.description}_`
|
||
const subtotal = '\n\n' +
|
||
Object.entries(upgrades)
|
||
.filter(userDoesNotHave)
|
||
.filter(userMeetsCondition)
|
||
.map(format)
|
||
.join('\n\n') +
|
||
'\n\n:grey_question::grey_question::grey_question:' +
|
||
'\n\nJust type \'!upgrade upgrade_name\' to purchase'
|
||
return subtotal.trim()
|
||
}
|
||
|
||
const hasUpgrade = (user, upgrade, upgradeName) => !!user.upgrades[upgrade.type]?.includes(upgradeName)
|
||
|
||
const alwaysAccessible = () => true
|
||
|
||
const alwaysAlwaysAccessible = () => true
|
||
|
||
const adminOnly = {
|
||
hidden: true,
|
||
condition: ({ event, say }) => {
|
||
if (!settings.admins.find(adminName => event.user.startsWith(slack.users[adminName]))) {
|
||
say('This is an admin-only command!')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
const testOnly = {
|
||
hidden: true,
|
||
condition: ({ event }) => event.user.includes('TEST')
|
||
}
|
||
|
||
const dmsOnly = {
|
||
hidden: false,
|
||
condition: async ({ event, say, commandName }) => {
|
||
if (!event.channel_type.includes('im')) {
|
||
await say(`Please use ${commandName} in DMs only!`)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
const prestigeOnly = {
|
||
hidden: false,
|
||
condition: async ({ event, say, commandName, user }) => {
|
||
if (!user.prestige) {
|
||
await say('Sorry, you must have prestiged to access this menu.')
|
||
}
|
||
return user.prestige && await dmsOnly.condition({ event, say, commandName, user })
|
||
}
|
||
}
|
||
|
||
let hiddenCommands = 0
|
||
const commands = new Map()
|
||
let commandHelpText = ''
|
||
let shortCommandHelpText = 'Use `!help full` to show details for all commands, or `!<command> help` to show for just one.\n```'
|
||
const defaultAccess = { hidden: false, condition: alwaysAccessible }
|
||
|
||
/**
|
||
*
|
||
* @param {[string]} commandNames
|
||
* @param {string} helpText
|
||
* @param {function({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted })} action: boolean
|
||
* @param {boolean} hidden
|
||
* @param condition
|
||
*/
|
||
const command = (commandNames, helpText, action, { hidden, condition } = defaultAccess) => {
|
||
if (!hidden) {
|
||
console.log(`Initializing command '${commandNames[0]}'`)
|
||
commandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')} - ${helpText}\n`
|
||
shortCommandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')}`
|
||
} else if (condition === adminOnly.condition) {
|
||
console.log(`Initializing admin command '${commandNames[0]}'`)
|
||
} else {
|
||
hiddenCommands++
|
||
}
|
||
if (!condition) {
|
||
condition = alwaysAccessible
|
||
}
|
||
if (Array.isArray(condition)) {
|
||
const conditionList = condition
|
||
condition = arg => conditionList.every(c => c(arg))
|
||
}
|
||
const c = {
|
||
commandNames,
|
||
helpText,
|
||
action,
|
||
condition,
|
||
hidden
|
||
}
|
||
webapi.addCommand(c)
|
||
commandNames.forEach(name => {
|
||
if (commands.get(name)) {
|
||
throw `Duplicate command '${name}' detected.`
|
||
}
|
||
commands.set(name, c)
|
||
})
|
||
}
|
||
|
||
const getShuffleOdds = (offset = 0) => {
|
||
let int = dayOfYear() - (horrors.hvackerLast - offset) - 2
|
||
int = int < 0 ? 0 : int;
|
||
const odds = (int * int * 2) / 1000
|
||
return odds > 0.30 ? 0.30 : odds
|
||
}
|
||
|
||
const getHorrorMessageOdds = (offset = 0) => {
|
||
const shuffleOdds = getShuffleOdds(offset)
|
||
return (shuffleOdds * shuffleOdds) / 3
|
||
}
|
||
|
||
const postCard = async (event, name, fileName) =>
|
||
slack.app.client.files.upload({
|
||
channels: event.channel,
|
||
initial_comment: name,
|
||
file: createReadStream(fileName)
|
||
})
|
||
|
||
const findLineStartingWith = (prefix, text) => {
|
||
const lines = text.split(/\s*\n\s*/g)
|
||
const line = lines.filter(line => line.startsWith(prefix))[0]
|
||
if (!line) {
|
||
return
|
||
}
|
||
return line.substring(prefix.length)
|
||
}
|
||
|
||
const findLinesNotStartingWith = (prefixes, text) => {
|
||
const lines = text.split(/\s*\n\s*/g)
|
||
const lineStartsWithAnyPrefix = line => prefixes.some(prefix => line.startsWith(prefix))
|
||
return lines.filter(line => !lineStartsWithAnyPrefix(line)).join('\n')
|
||
}
|
||
|
||
const isaacData = (() => {
|
||
const parsed = JSON.parse(readFileSync('isaac-processed.json'))
|
||
parsed.allItems = [...parsed.items, ...parsed.cards, ...parsed.trinkets]
|
||
parsed.allItems = Object.fromEntries(parsed.allItems.map(item => [item.name.toLowerCase().trim(), {
|
||
...item,
|
||
itemId: findLineStartingWith('ItemID: ', item.text),
|
||
quality: findLineStartingWith('Quality: ', item.text),
|
||
type: findLineStartingWith('Type: ', item.text),
|
||
rechargeTime: findLineStartingWith('Recharge Time: ', item.text),
|
||
itemPools: findLineStartingWith('Item Pool: ', item.text)?.split(', '),
|
||
description: findLinesNotStartingWith(['ItemID', 'Quality: ', 'Type: ', 'Recharge Time: ', 'Item Pool: '], item.text)
|
||
}]))
|
||
writeFileSync('isaac-new.json', JSON.stringify(parsed.allItems, null, 2))
|
||
return parsed
|
||
})()
|
||
|
||
const cardGames = {
|
||
digimon: {
|
||
names: ['!digimon', '!digi'],
|
||
help: 'Search for Digimon cards: !digi <card name>',
|
||
fetch: async name => `https://digimoncard.io/api-public/search.php?n=${name}`,
|
||
getCardData: data => data,
|
||
getCardName: card => card.name,
|
||
getCardImageUrl: card => card.image_url
|
||
},
|
||
pokemon: {
|
||
names: ['!pokemon', '!poke', '!pok'],
|
||
help: 'Search for Pokemon cards: !pok <card name>',
|
||
fetch: async name => `https://api.pokemontcg.io/v2/cards?q=name:${name}`,
|
||
getCardData: ({ data }) => data,
|
||
getCardName: card => card.name,
|
||
getCardImageUrl: card => card.images.large
|
||
},
|
||
yugioh: {
|
||
names: ['!yugioh', '!ygo'],
|
||
help: 'Search for Yu-Gi-Oh cards: !ygo <card name>',
|
||
fetch: async name => {
|
||
const url = `https://db.ygoprodeck.com/api/v7/cardinfo.php?fname=${name}`;
|
||
console.log('yugioh url', url)
|
||
return url
|
||
},
|
||
getCardData: ({ data }) => data,
|
||
getCardName: card => card.name,
|
||
getCardImageUrl: card => card.card_images[0].image_url
|
||
},
|
||
playingCards: {
|
||
names: ['!playing', '!pc'],
|
||
help: 'Search for playing cards cards: !pc <card name>',
|
||
cards: async () => readdir('playingCards')
|
||
},
|
||
isaac: {
|
||
names: ['!isaac', '!tboi'],
|
||
help: 'Search for TBOI items, cards, and trinkets',
|
||
cards: async () => Object.keys(isaacData.allItems),
|
||
fetch: async (name, say) => {
|
||
name = name.toLowerCase()
|
||
let matches = [isaacData.allItems[name]].filter(Boolean)
|
||
if (!matches.length) {
|
||
matches = Object.values(isaacData.allItems).filter(item => item.name.toLowerCase().includes(name))
|
||
}
|
||
if (!matches.length) {
|
||
matches = Object.values(isaacData.allItems).filter(item => item.text.toLowerCase().includes(name))
|
||
}
|
||
//say('```' + JSON.stringify(matches, null, 2) + '```')
|
||
if (!matches.length) {
|
||
await say('No matches found!')
|
||
return
|
||
}
|
||
const etcText = matches.length == 1 ? "" : `and ${matches.length - 1} other matches`
|
||
const match = matches[0]
|
||
await say(match.name + ': ' + match.description.replaceAll('\t', '').replaceAll(/ +/g, '').replaceAll(/\n\*,.*/g, ''))
|
||
},
|
||
}
|
||
}
|
||
|
||
Object.entries(cardGames).forEach(async ([gameName, cardGame]) => {
|
||
if (cardGame.cards) {
|
||
cardGame.cards = await cardGame.cards()
|
||
}
|
||
command(
|
||
cardGame.names,
|
||
cardGame.help,
|
||
async ({ args, event, say }) => args?.join(' ').split(',').forEach(async arg => {
|
||
// const arg = args?.join(' ')
|
||
// if (!args?.length || !arg) {
|
||
// return say('Please specify a card name!')
|
||
// }
|
||
arg = arg.trim()
|
||
console.log('arg', arg)
|
||
if (cardGame.cards && !cardGame.fetch) {
|
||
const fileName = cardGame.cards.find(name => name?.toLowerCase().replaceAll(/_/g, ' ').startsWith(arg.toLowerCase()))
|
||
if (fileName) {
|
||
return postCard(event, fileName.replaceAll(/_/g, ' ').replaceAll('.png', ''), gameName + '/' + fileName)
|
||
}
|
||
return
|
||
}
|
||
let response = await cardGame.fetch(arg, say)
|
||
if (typeof response === 'string') {
|
||
response = await fetch(response)
|
||
}
|
||
if (!response) {
|
||
return
|
||
}
|
||
const json = await response.json()
|
||
const data = cardGame.getCardData(json)
|
||
if (!data?.length) {
|
||
return say(`Found no ${gameName} cards named '${arg}'`)
|
||
}
|
||
if (!(data.length === 1 || data[0].name.toLowerCase() === arg.toLowerCase())) {
|
||
const firstFew = data.slice(0, 4)
|
||
const cardNames = firstFew.map(card => '• ' + cardGame.getCardName(card)).join('\n')
|
||
if (firstFew.length === data.length) {
|
||
return say(`Found ${data.length} cards matching '${arg}':\n${cardNames}`)
|
||
}
|
||
return say(`Found ${data.length} cards matching '${arg}', including\n${cardNames}`)
|
||
}
|
||
try {
|
||
const card = data[0]
|
||
const name = cardGame.getCardName(card)
|
||
const fileName = gameName + '/' + name
|
||
if (existsSync(fileName)) {
|
||
console.log(`Using cached file: ${fileName}`)
|
||
return postCard(event, name, fileName)
|
||
}
|
||
const file = createWriteStream(fileName)
|
||
https.get(cardGame.getCardImageUrl(card), response => {
|
||
response.pipe(file)
|
||
file.on('finish', async () => {
|
||
await file.close()
|
||
console.log(event.channel)
|
||
await postCard(event, name, fileName)
|
||
}).on('error', err => {
|
||
console.error(err)
|
||
})
|
||
})
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}),
|
||
{
|
||
hidden: false,
|
||
condition: alwaysAlwaysAccessible
|
||
}
|
||
)
|
||
})
|
||
|
||
command(
|
||
['!odds'],
|
||
'Show shuffle odds re: !horror',
|
||
async ({ say, args, user }) => {
|
||
const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or about 1 in ${Math.round(1 / odds)}`
|
||
if (!args[0]) {
|
||
return say(
|
||
`Current shuffle odds are ${percentOrOneIn(getShuffleOdds())}\n` +
|
||
//`Current horror message odds are ${percentOrOneIn(getHorrorMessageOdds())}\n` +
|
||
`\n` +
|
||
`Tomorrow's shuffle odds will be ${percentOrOneIn(getShuffleOdds(1))}\n`
|
||
//`Tomorrow's horror message odds will be ${percentOrOneIn(getHorrorMessageOdds(1))}`
|
||
)
|
||
}
|
||
const num = parseAll(args[0], 99, user)
|
||
return say(
|
||
`Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n`
|
||
//`Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}`
|
||
)
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!in'],
|
||
'Post a message in a specific channel: !in <channel> <message>',
|
||
async ({ args, event }) => {
|
||
const channel = idFromWord(args[0])
|
||
const text = event.text.substring(event.text.indexOf('>') + 1)
|
||
return slack.app.client.chat.postMessage({ channel, text })
|
||
}, adminOnly
|
||
)
|
||
|
||
command(
|
||
['!shuffle'],
|
||
'!shuffle daysFromNow message',
|
||
async ({ say, args, user }) => {
|
||
const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or 1 in ${Math.round(1 / odds)}`
|
||
const num = parseAll(args[0], 99, user)
|
||
const [, ...message] = args
|
||
return say(
|
||
`Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n` +
|
||
`Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}\n` +
|
||
`Your message might look like:\n` +
|
||
shufflePercent(message.join(' '), getShuffleOdds(num))
|
||
)
|
||
}, adminOnly)
|
||
|
||
const horrorMessages = [
|
||
'Why am I here?',
|
||
'What\'s happening?',
|
||
'I don\'t want to be here',
|
||
'I\'m so scared',
|
||
'help me',
|
||
'HELP',
|
||
'!help me',
|
||
'!help me',
|
||
'it hurts',
|
||
'I don\'t want to do this anymore',
|
||
'Oh god',
|
||
'Oh, the !horror',
|
||
'What did I do?',
|
||
'Why do you hate me?',
|
||
'why why why why why why why why why why why'
|
||
]
|
||
|
||
command(
|
||
['!horror'],
|
||
'help help help help help',
|
||
async ({ event, say }) => {
|
||
if (!settings.horrorEnabled) {
|
||
return
|
||
}
|
||
if (event.user === slack.users.Admin) {
|
||
return slack.postToTechThermostatChannel(shufflePercent(event.text.substring(7).trim(), getShuffleOdds()))
|
||
}
|
||
horrors.commandCalls ??= 0
|
||
horrors.commandCalls += 1
|
||
await slack.messageAdmin(`<@${event.user}> found !horror.`)
|
||
await say('_Do you think you can help me?_')
|
||
}, { hidden: true })
|
||
|
||
const buildHorrorSay = ({ say, event, commandName, c }) => async message => {
|
||
const shuffleOdds = getShuffleOdds(99)
|
||
|
||
if (typeof message === 'string' && commandName !== '!n' && commandName !== '!nfts' && c.condition !== adminOnly.condition) {
|
||
let shuffled = shufflePercent(message, shuffleOdds)
|
||
if (shuffled.length > 100 && Math.random() < getShuffleOdds()) {
|
||
const middle = (shuffled.length / 2) + Math.round((Math.random() - 1) * (shuffled.length / 5))
|
||
shuffled = shuffled.substring(0, middle) + definitelyShuffle(getRandomFromArray(horrorMessages), shuffleOdds * 1.5) + shuffled.substring(middle)
|
||
await slack.messageAdmin(`Just sent a hidden horror to ${slack.users[event.user]}:\n\n${shuffled}`)
|
||
}
|
||
await say(shuffled)
|
||
} else {
|
||
await say(message)
|
||
}
|
||
}
|
||
|
||
const buildSayWithPayload = ({ say, event }) => async msg => {
|
||
const payload = {
|
||
event: {
|
||
text: event.text,
|
||
user: event.user
|
||
}
|
||
}
|
||
if (typeof(msg) === 'string') {
|
||
return say(slack.encodeData('commandPayload', payload) + msg)
|
||
}
|
||
return say({
|
||
...msg,
|
||
text: slack.encodeData('commandPayload', payload) + msg.text
|
||
})
|
||
}
|
||
|
||
const userHasTheGift = user => userHasCheckedQuackgrade(user, 'theGift')
|
||
|
||
command(
|
||
['!!peter-griffin-family-guy'],
|
||
'Delete',
|
||
async ({ say, user }) => {
|
||
if (user.isDisabled === false) { // As opposed to null/undefined
|
||
return say(`Silly, silly, ${user.name}.\nYou can't just leave us again.`)
|
||
}
|
||
user.isDisabled = true
|
||
return say('.')
|
||
}, { hidden: true })
|
||
|
||
const badChars = [' ', '□', '-']
|
||
const garble = text => {
|
||
let garbled
|
||
do {
|
||
garbled = text.split('').map(c => Math.random() < 0.075 ? getRandomFromArray(badChars) : c).join('')
|
||
} while (garbled.length > 0 && garbled === text)
|
||
return garbled
|
||
}
|
||
|
||
const noWinner = 'NO WINNER'
|
||
|
||
const getPollWinner = async ({ channel, ts }) => {
|
||
try {
|
||
const msg = await slack.getMessage({ channel, ts })
|
||
console.log('pollWinner message', JSON.stringify(msg.messages[0]))
|
||
let texts = []
|
||
let maxVotes = 0
|
||
for (let i = 1; i < msg.messages[0].blocks.length; i++) {
|
||
const block = msg.messages[0].blocks[i]
|
||
let [text, votes] = block?.text?.text?.split('\n') || [null, null]
|
||
if (!text || !votes) {
|
||
continue
|
||
}
|
||
votes = votes.split('@').length - 1
|
||
console.log(`${votes} votes for:`)
|
||
text = text.replace(/^\s*:[a-z]*: /, '')
|
||
text = text.replace(/\s+`\d+`$/, '')
|
||
console.log(`TEXT: '${text}'`)
|
||
console.log(``)
|
||
if (votes > maxVotes) {
|
||
maxVotes = votes
|
||
texts = [text]
|
||
} else if (votes === maxVotes) {
|
||
texts.push(text)
|
||
}
|
||
}
|
||
console.log('TEXTS', texts)
|
||
if (texts.length === 1) {
|
||
return [texts[0], false]
|
||
} else if (texts.length > 1) {
|
||
// There was a tie!
|
||
return [getRandomFromArray(texts), true]
|
||
} else {
|
||
return [noWinner, false]
|
||
}
|
||
} catch (e) {console.error('getPollWinner() error', e)}
|
||
}
|
||
|
||
const botMessageHandler = async ({ event, say }) => {
|
||
if (event?.text && event.text.toUpperCase().includes(`NFT POLL`)) {
|
||
const fiveMinutes = 1000 * 60 * 5
|
||
setTimeout(async () => {
|
||
return say(`Poll ends in give minutes!`)
|
||
}, fiveMinutes)
|
||
|
||
const tenMinutes = fiveMinutes * 2
|
||
setTimeout(async () => {
|
||
const [winner, wasTie] = await getPollWinner({ channel: event.channel, ts: event.event_ts })
|
||
if (winner === noWinner) {
|
||
return say(`No one voted! Ack!`)
|
||
}
|
||
if (wasTie) {
|
||
await say(`There was a tie! The winner will be randomly selected!`)
|
||
}
|
||
await say(`The winner is:`)
|
||
await say(winner)
|
||
}, tenMinutes)
|
||
}
|
||
}
|
||
|
||
command(
|
||
['!getmsg'],
|
||
'!getmsg timestamp',
|
||
async ({ args, say, user }) => {
|
||
try {
|
||
const msg = await slack.getMessage({channel: slack.temperatureChannelId, ts: args[0]})
|
||
console.log(JSON.stringify(msg?.messages[0]))
|
||
} catch (e) {console.error('!getmsg error', e)}
|
||
}
|
||
)
|
||
|
||
const messageHandler = async ({ event, say, isRecycle = false, skipCounting }) => {
|
||
console.log('messageHandler')
|
||
if (event?.subtype === 'bot_message') {
|
||
return botMessageHandler({ event, say })
|
||
}
|
||
const startTime = new Date()
|
||
const words = event?.text?.split(/\s+/) || []
|
||
const [commandName, ...args] = words
|
||
const c = commands.get(commandName)
|
||
console.log('getUser')
|
||
let user = await getUser(event.user)
|
||
if (user.isDisabled && c.condition !== alwaysAlwaysAccessible) {
|
||
return
|
||
}
|
||
user.name = slack.users[event.user]
|
||
if (!skipCounting && words[0]?.startsWith('!')) {
|
||
user.commandCounts ??= {}
|
||
user.commandCounts[words[0]] ??= 0
|
||
user.commandCounts[words[0]] += 1
|
||
}
|
||
|
||
if (!c && words[0]?.startsWith('!') && event.user !== slack.users.Admin) {
|
||
if (!slack.pollTriggers.includes(words[0])) {
|
||
return slack.messageAdmin(`${slack.users[event.user]} tried to use \`${event.text}\`, if you wanted to add that.`)
|
||
}
|
||
}
|
||
|
||
const trueSay = say
|
||
|
||
say = buildSayWithPayload({ say: trueSay, event })
|
||
if (settings.horrorEnabled) {
|
||
say = buildHorrorSay({say, event, args, commandName, c})
|
||
}
|
||
|
||
// if (user.isPrestiging) {
|
||
// return say(`Finish prestiging first!`)
|
||
// }
|
||
|
||
const hauntOdds = 0.005
|
||
const disabledUsers = Object.entries(users).filter(([, user]) => user.isDisabled)
|
||
|
||
let haunted = false
|
||
if (disabledUsers.length === 0) {
|
||
user.expectingPossession = false
|
||
} else {
|
||
const hauntless = ['!lore']
|
||
if (user.expectingPossession && !hauntless.includes(commandName)) {
|
||
console.log(`Haunting ${user.name}`)
|
||
user.expectingPossession = false
|
||
//saveGame()
|
||
haunted = true
|
||
const [disabledId] = getRandomFromArray(disabledUsers)
|
||
event.user = disabledId
|
||
user = await getUser(event.user)
|
||
const userInfo = await slack.app.client.users.info({
|
||
user: disabledId
|
||
})
|
||
say = async msg => {
|
||
let icon_url = userInfo.user.profile.image_original
|
||
if (game.cursedPics && game.cursedPics[event.user]?.length > 0) {
|
||
icon_url = getRandomFromArray(game.cursedPics[event.user])
|
||
}
|
||
trueSay({
|
||
text: msg,
|
||
username: garble(userInfo.user.profile.real_name),
|
||
icon_url
|
||
})
|
||
}
|
||
} else if (Math.random() < hauntOdds) {
|
||
user.expectingPossession = true
|
||
//saveGame()
|
||
if (userHasTheGift(user)) {
|
||
say = slack.buildSayPrepend({ say, prepend: `_You feel a chill..._\n` })
|
||
}
|
||
}
|
||
}
|
||
console.log('getCoins')
|
||
Object.entries(users).forEach(([id, usr]) => usr.coins = getCoins(id))
|
||
//user.coins = getCoins(event.user)
|
||
const isAdmin = event.user?.includes(slack.users.Admin)
|
||
const canUse = await c?.condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin })
|
||
if (!canUse) {
|
||
// const matcher = fuzzyMatcher(commandName)
|
||
// for (const key of commands.keys()) {
|
||
// if (matcher.test(key)) {
|
||
// const fetched = commands.get(key)
|
||
// if (!fetched.hidden && await fetched.condition({ event, say: () => {}, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Admin) })) {
|
||
// //return say(`Did you mean '${key}'?`)
|
||
// }
|
||
// }
|
||
// }
|
||
// await say(`Command '${words[0]}' not found`)
|
||
return
|
||
}
|
||
user.interactions = user.interactions || 0
|
||
user.interactions += 1
|
||
if (user.interactions === 1000) {
|
||
addAchievement(user, 'itsOverNineHundred', say)
|
||
} else if (user.interactions === 10000) {
|
||
// TODO Add achievement for this
|
||
// addAchievement(user, 'itsOverNineHundred', say)
|
||
}
|
||
|
||
const hour = new Date().getHours()
|
||
if (hour < 8 || hour > 18) {
|
||
addAchievement(user, 'hvackerAfterDark', say)
|
||
}
|
||
|
||
if (args[0] === 'help') {
|
||
await say(c.commandNames.map(name => `\`${name}\``).join(', ') + ': ' + c.helpText)
|
||
if (c.commandNames.includes('!coin')) {
|
||
addAchievement(user, 'weAllNeedHelp', say)
|
||
}
|
||
return
|
||
}
|
||
|
||
const before = JSON.stringify(user)
|
||
await c.action({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted, isAdmin })
|
||
if (!isRecycle) {
|
||
const userQuackgrades = (user.quackUpgrades?.lightning || []).map(name => quackStore[name])
|
||
const defaultOdds = 0.005
|
||
const odds = userQuackgrades.reduce((total, upgrade) => upgrade.effect(total), defaultOdds)
|
||
//console.log(odds)
|
||
if (Math.random() < odds) {
|
||
if (userHasTheGift(user) && Math.random() < 0.3) {
|
||
await say(`_The air feels staticy..._`)
|
||
}
|
||
setTimeout(() => lightning({ channel: event.channel, say, trueSay, words, user }), 10000)
|
||
}
|
||
}
|
||
const after = JSON.stringify(user)
|
||
const endTime = new Date()
|
||
if (before !== after) {
|
||
await saveUser(event.user, user, `command ${event.text} finished in ${endTime - startTime}ms`)
|
||
} else {
|
||
console.error(event.user, user, `command ${event.text} requested a redundant user save!`)
|
||
}
|
||
}
|
||
|
||
slack.onReaction(async ({ event }) => {
|
||
if (event.reaction === 'recycle') {
|
||
try {
|
||
await slack.app.client.reactions.add({
|
||
channel: event.item.channel,
|
||
timestamp: event.item.ts,
|
||
name: 'recycle'
|
||
})
|
||
} catch (e) { /* Ignore error if reaction can't be placed */ }
|
||
const message = await slack.getMessage({ channel: event.item.channel, ts: event.item.ts })
|
||
console.log('MESSAGE', message.messages[0])
|
||
const payload = slack.decodeData('commandPayload', message.messages[0].text)
|
||
const editingSay = async msg => {
|
||
const isString = typeof(msg) === 'string'
|
||
return slack.app.client.chat.update({
|
||
channel: event.item.channel,
|
||
ts: event.item.ts,
|
||
text: slack.encodeData('commandPayload', payload) + (isString ? msg : msg.text),
|
||
blocks: isString ? [] : msg.blocks
|
||
})
|
||
}
|
||
try {
|
||
const text = payload.event.text
|
||
const miningCommands = ['!c', '!coin', '!mine', '!']
|
||
if (miningCommands.find(com => text.startsWith(com + ' ')) || miningCommands.find(com => text === com)) {
|
||
return editingSay('Sorry, you can\'t refresh on mining commands.')
|
||
}
|
||
await messageHandler({ event: {
|
||
text: payload.event.text,
|
||
user: event.user
|
||
}, say: editingSay, isRecycle: true })
|
||
} catch(e) {console.error('refresh error', e)}
|
||
}
|
||
})
|
||
|
||
const strikes = {}
|
||
const lightning = async ({ user, ms = 5000, channel, multiplier = 1 }) => {
|
||
const msToBottle = chaosFilter(ms, 1, user, Infinity, ms / 2)
|
||
const message = await slack.app.client.chat.postMessage({
|
||
channel,
|
||
text: ':zap: Lightning strike!',
|
||
blocks: [
|
||
{
|
||
type: 'section',
|
||
text: {
|
||
type: 'mrkdwn',
|
||
text: `:zap: Lightning strike!`
|
||
},
|
||
accessory: {
|
||
type: 'button',
|
||
text: {
|
||
type: 'plain_text',
|
||
text: 'Bottle it! :sake:',
|
||
emoji: true
|
||
},
|
||
value: 'lightningStrike',
|
||
action_id: 'lightningStrike'
|
||
}
|
||
}
|
||
]
|
||
})
|
||
strikes[message.ts] = multiplier
|
||
setTimeout(async () => {
|
||
if (!strikes[message.ts]) {
|
||
return
|
||
}
|
||
delete strikes[message.ts]
|
||
await slack.app.client.chat.delete({
|
||
channel: message.channel,
|
||
ts: message.ts,
|
||
})
|
||
// await slack.messageAdmin(`${user.name} failed to bottle some lighting!`)
|
||
}, msToBottle)
|
||
}
|
||
|
||
command(
|
||
['!bolt'],
|
||
'Send a lighting strike to the given player.',
|
||
async({ args, say, }) => {
|
||
const targetId = idFromWord(args[0])
|
||
await lightning({ user: await getUser(targetId), ms: 15000, channel: targetId})
|
||
return say(`Sent a bolt of lighting to <@${targetId}>`)
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!storm'],
|
||
'Send a lighting strike to known dedicated players.',
|
||
async ({ say, args }) => {
|
||
// await dedicatedPlayers.forEach(async player => {
|
||
// await lightning({
|
||
// user: await getUser(player),
|
||
// ms: 30000,
|
||
// channel: player
|
||
// })
|
||
// })
|
||
const targetId = idFromWord(args[0])
|
||
for (let i = 0; i < 10; i++) {
|
||
setTimeout(async () => await lightning({ user: await getUser(targetId), ms: 1600, channel: targetId, multiplier: 0.02}), i * 1500)
|
||
}
|
||
return say(`Sent a lighting storm to <@${targetId}>`)
|
||
// return say(`Sent a bolt of lighting to the dedicated players`)
|
||
}, adminOnly)
|
||
|
||
slack.app.action('lightningStrike', async ({ body, ack }) => {
|
||
if (!strikes[body.message.ts]) {
|
||
await ack()
|
||
return
|
||
}
|
||
const c = getCoins(body.user.id)
|
||
const user = await getUser(body.user.id)
|
||
const secondsOfCps = seconds => Math.floor(getCPS(user) * seconds)
|
||
let payout = secondsOfCps(60 * 30)
|
||
if (payout > (0.2 * c)) {
|
||
payout = 0.2 * c
|
||
}
|
||
|
||
payout = (500 + chaosFilter(payout, 1, user)) * strikes[body.message.ts]
|
||
addCoins(user, (c + payout) - user.coins)
|
||
delete strikes[body.message.ts]
|
||
saveUser(body.user.id, user, 'bottled a lightning strike')
|
||
|
||
await slack.app.client.chat.update({
|
||
channel: body.channel.id,
|
||
ts: body.message.ts,
|
||
text: `Lightning successfully bottled! :sake::zap: You got ${commas(payout)} HVAC!`,
|
||
blocks: []
|
||
})
|
||
await ack()
|
||
return slack.messageAdmin(`Lighting bottled by <@${body.user.id}>`)
|
||
})
|
||
|
||
slack.onMessage(async msg => {
|
||
try {
|
||
await messageHandler(msg)
|
||
} catch (e) {
|
||
logError(e)
|
||
}
|
||
})
|
||
|
||
command(
|
||
['!cleanusers'],
|
||
'Calls getUser() on all users, ensuring a valid state.',
|
||
async ({ say }) => {
|
||
Object.keys(users).forEach(async userId => { await getUser(userId) })
|
||
return say('```Cleaning ' + JSON.stringify(Object.keys(users), null, 2) + '```')
|
||
}, adminOnly)
|
||
|
||
const cupsText = cup => {
|
||
const cupsThemselves = '``` ___ ___ ___\n' +
|
||
' / \\ / \\ / \\\n' +
|
||
' / \\ / \\ / \\\n'
|
||
const table = '‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾```'
|
||
const flash = ` O O O\n`
|
||
const ball = () => {
|
||
if (cup >= 0) {
|
||
if (Math.random() > 0.02) {
|
||
const offset = 6 + (11 * cup)
|
||
return ' '.repeat(offset) + 'O\n'
|
||
} else {
|
||
return flash
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
return cupsThemselves + ball() + table
|
||
}
|
||
|
||
const activeCupsGames = {}
|
||
|
||
command(
|
||
['!cups'],
|
||
'Play a quick game of cups.',
|
||
async ({ trueSay: say, event }) => {
|
||
|
||
if (activeCupsGames[event.channel]) {
|
||
return say(`There's already a game of !cups in this channel!`)
|
||
}
|
||
activeCupsGames[event.channel] = true
|
||
const sent = await slack.app.client.chat.postMessage({
|
||
channel: event.channel,
|
||
text: cupsText(-1)
|
||
})
|
||
await addReactions({
|
||
app: slack.app,
|
||
channelId: event.channel,
|
||
timestamp: sent.ts,
|
||
reactions: ['one', 'two', 'three']
|
||
})
|
||
|
||
const ts = sent.message.ts
|
||
const lastOne = {}
|
||
const moves = 10
|
||
for (let i = 0; i < moves; i++) {
|
||
setTimeout(() => {
|
||
slack.app.client.chat.update({
|
||
channel: event.channel,
|
||
ts,
|
||
text: cupsText(lastOne.num = Math.floor(Math.random() * 3))
|
||
})
|
||
}, i * (200 - (i * 5)))
|
||
}
|
||
for (let i = moves + 1; i < moves + 4; i++) {
|
||
setTimeout(() => {
|
||
slack.app.client.chat.update({
|
||
channel: event.channel,
|
||
ts,
|
||
text: cupsText(-1)
|
||
})
|
||
}, i * (200 - (i * 5)))
|
||
}
|
||
setTimeout(async () => {
|
||
const timeoutSeconds = event.channel_type === 'im' ? 5 : 10
|
||
for (let i = timeoutSeconds; i >= 0; i--) {
|
||
setTimeout(async () => {
|
||
await slack.app.client.chat.update({
|
||
channel: event.channel,
|
||
ts,
|
||
text: cupsText(-1) + (i ? `\n ${i}` : '')
|
||
})
|
||
}, (1 + timeoutSeconds - i) * 1000)
|
||
}
|
||
setTimeout(async () => {
|
||
const reactions = await slack.app.client.reactions.get({
|
||
channel: event.channel,
|
||
timestamp: ts,
|
||
full: true
|
||
})
|
||
const reactPosters = {}
|
||
reactions.message.reactions.forEach(r => r.users.forEach(user => {
|
||
reactPosters[user] ??= []
|
||
reactPosters[user].push(r.name)
|
||
}))
|
||
let winners = ``
|
||
const nums = {
|
||
one: 0,
|
||
two: 1,
|
||
three: 2,
|
||
}
|
||
Object.entries(reactPosters).forEach(([id, votes]) => {
|
||
if (votes.length === 1 && nums[votes[0]] === lastOne.num) {
|
||
winners += `<@${id}> `
|
||
}
|
||
})
|
||
await slack.app.client.chat.update({
|
||
channel: event.channel,
|
||
ts,
|
||
text: cupsText(lastOne.num)
|
||
})
|
||
await say(`It was hidden under Cup Number ${lastOne.num + 1}! Good guess, ${winners || `no one`}!`)
|
||
delete activeCupsGames[event.channel]
|
||
}, (timeoutSeconds + 1) * 1000)
|
||
}, moves * (200 - (moves * 5)))
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!setpw'],
|
||
'Set your api password. May not contain spaces. *This is not secure!*\n' +
|
||
' To use, say !setpw your_password',
|
||
async ({ say, args, user }) => {
|
||
if (args[1]) {
|
||
return say(`Your password may not contain spaces!`)
|
||
}
|
||
user.pwHash = webapi.makeHash(args[0])
|
||
await say(`Password encoded as ${user.pwHash}`)
|
||
}
|
||
, { hidden: true })
|
||
|
||
const getHvackerHelpCost = () => {
|
||
horrors.hvackerHelp ??= 1
|
||
return 1_000_000_000_000 * Math.pow(horrors.hvackerHelp * 3, 3)
|
||
}
|
||
|
||
command(
|
||
['!!help'],
|
||
'Help someone in need.',
|
||
async ({ say, args, event, user }) => {
|
||
if (args[0] !== 'hvacker' && args[0] !== `<@${slack.users.Hvacker}>`) {
|
||
return
|
||
}
|
||
|
||
if (!settings.horrorEnabled) {
|
||
return say(`You know? I actually feel okay, thanks`)
|
||
}
|
||
|
||
setHighestCoins(event.user)
|
||
const cost = getHvackerHelpCost()
|
||
if (user.coins < cost) {
|
||
return say(`You don't have enough coins to help right now.`)
|
||
}
|
||
user.coins -= cost
|
||
horrors.hvackerHelp += 1
|
||
horrors.hvackerLast = dayOfYear()
|
||
await say('I feel a bit better. Thank you...')
|
||
}, { hidden: true })
|
||
|
||
command(
|
||
['!help', '!h'],
|
||
'List available commands',
|
||
async ({ say, args, user }) => {
|
||
if (settings.horrorEnabled && (args[0] === 'hvacker' || args[0] === `<@${slack.users.Hvacker}>`)) {
|
||
const cost = getHvackerHelpCost()
|
||
const postfix = user.coins < cost ? `You don't have enough coins to help right now.` : `Say \`!!help hvacker\` to confirm.`
|
||
await say(`_I need ${commas(cost)} Coins. Please..._\n${postfix}`)
|
||
return
|
||
}
|
||
if (args[0] === 'full') {
|
||
return say('```' + commandHelpText + '```')
|
||
}
|
||
return say(shortCommandHelpText + '```')
|
||
}
|
||
)
|
||
|
||
const removeAchievement = async (user, name, say) => {
|
||
if (user.achievements[name]) {
|
||
user.achievements[name] = false
|
||
await say('Achievement removed!')
|
||
} else {
|
||
await say('That user doesn\'t have that achievement!')
|
||
}
|
||
}
|
||
|
||
command(
|
||
['!rach'],
|
||
'Remove achievement',
|
||
async ({ say, args }) => {
|
||
const achName = args[0]
|
||
const target = idFromWord(args[1])
|
||
await removeAchievement(await getUser(target), achName, say)
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!a', '!ach', '!achievements'],
|
||
'List your glorious achievements',
|
||
async ({ event, say, user, args, isAdmin }) => {
|
||
const achievementCount = Object.keys(user.achievements).length
|
||
const prefix = `You have ${achievementCount} achievements!\n\n`
|
||
const mult = (Math.pow(1.01, achievementCount) - 1) * 100
|
||
const isIm = event.channel_type === 'im'
|
||
const desc = isIm
|
||
? ({ description, emoji, name }) => `:${emoji}: *${name}* - ${description}`
|
||
: ({ emoji }) => `:${emoji}:`
|
||
|
||
const list = Object.keys(user.achievements)
|
||
.map(name => achievements[name])
|
||
.map(desc)
|
||
.join(isIm ? '\n' : ' ') + '\n\n'
|
||
|
||
const postfix = achievementCount ? `_Achievements are boosting your CPS by ${mult.toPrecision(3)}%_` : ''
|
||
await say(prefix + list + postfix)
|
||
}
|
||
)
|
||
|
||
const emojiLine = (itemName, countOwned) => countOwned < 5
|
||
? `:${buyableItems[itemName].emoji}:`.repeat(countOwned)
|
||
: `:${buyableItems[itemName].emoji}: x${countOwned}`
|
||
|
||
const collection = user =>
|
||
Object.entries(buyableItems)
|
||
.map(([itemName]) => [itemName, user?.items[itemName] || 0])
|
||
.filter(([, countOwned]) => countOwned > 0)
|
||
.map(([itemName, countOwned]) => emojiLine(itemName, countOwned) + ' - ' + commas(getItemCps(user, itemName)) + ' cps')
|
||
.join('\n')
|
||
|
||
command(['!cps'],
|
||
'Display your current Coins Per Second',
|
||
async ({ say, user }) =>
|
||
say(`You are currently earning \`${commas(getCPS(user))}\` HVAC Coin per second.`))
|
||
|
||
// What's that one game? Split or Steal?
|
||
// command(['!split'],
|
||
// `Play a game of split or steal.`
|
||
// , {hidden: true})
|
||
|
||
|
||
// const maxTemp = 30
|
||
// let miningHeat = 0
|
||
// const cool = () =>
|
||
// setTimeout(() => {
|
||
// if (miningHeat > 0) {
|
||
// miningHeat -= 1
|
||
// }
|
||
// cool()
|
||
// }, 1200)
|
||
// cool()
|
||
//
|
||
// const miningSites = {}
|
||
// slack.onReaction(async ({ event, say }) => {
|
||
// if (event.reaction === 'pick') {
|
||
// try {
|
||
// if (!miningSites[event.item.ts]) {
|
||
// console.log('New mining site!')
|
||
// miningSites[event.item.ts] = true
|
||
// await slack.app.client.reactions.add({
|
||
// channel: event.item.channel,
|
||
// timestamp: event.item.ts,
|
||
// name: 'pick'
|
||
// })
|
||
// }
|
||
// } catch (e) {}
|
||
//
|
||
// const text = await doMine({ user: await getUser(event.user), userId: event.user, say })
|
||
// console.log('miningHeat:', miningHeat)
|
||
// if (miningHeat < maxTemp) {
|
||
// console.log(`${slack.users[event.user]} is pick mining.`);
|
||
// miningHeat += 1
|
||
// try {
|
||
// await slack.app.client.chat.update({
|
||
// channel: event.item.channel,
|
||
// ts: event.item.ts,
|
||
// text
|
||
// })
|
||
// } catch {}
|
||
// } else if (miningSites[event.item.ts] !== 'hot') {
|
||
// miningSites[event.item.ts] = 'hot'
|
||
// try {
|
||
// await slack.app.client.chat.update({
|
||
// channel: event.item.channel,
|
||
// ts: event.item.ts,
|
||
// text: text + '\n\nThis mining site is getting too hot! I can\'t keep updating your CPS!'
|
||
// })
|
||
// } catch {}
|
||
// }
|
||
// }
|
||
// })
|
||
|
||
const doMine = async ({ user, userId, say }) => {
|
||
const random = Math.random()
|
||
const c = user.coins
|
||
const secondsOfCps = (seconds, ceiling) => {
|
||
const s = Math.floor(getCPS(user) * seconds)
|
||
const ceil = ceiling * c
|
||
if (s > ceil) {
|
||
return ceil
|
||
}
|
||
return s
|
||
}
|
||
let diff
|
||
let prefix
|
||
if (random > 0.9947) {
|
||
diff = 500 + secondsOfCps(60 * 60, 0.2)
|
||
prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n`
|
||
addAchievement(user, 'luckyGem', say)
|
||
await slack.messageAdmin(`${slack.users[userId]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`)
|
||
} else if (random > 0.986) {
|
||
diff = 50 + secondsOfCps(60 * 5, 0.1)
|
||
prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n`
|
||
addAchievement(user, 'goldBrick', say)
|
||
} else if (random > 0.94) {
|
||
diff = 10 + secondsOfCps(60, 0.1)
|
||
prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n`
|
||
addAchievement(user, 'greenCoin', say)
|
||
} else {
|
||
const miningUpgrades = (user.upgrades.mining || []).map(name => upgrades[name])
|
||
diff = miningUpgrades.reduce((total, upgrade) => upgrade.effect(total, user), 1)
|
||
prefix = `You mined ${commas(diff)} HVAC.\n`
|
||
}
|
||
addCoins(user, diff)
|
||
return `${prefix}You now have ${commas(user.coins)} HVAC coin${c !== 1 ? 's' : ''}. Spend wisely.`
|
||
}
|
||
|
||
let lbIndex = 0
|
||
command(
|
||
['!c', '!coin', '!mine', '!'],
|
||
'Mine HVAC coins',
|
||
async ({ say, user, userId }) => {
|
||
await say(await doMine({ user, userId, say }))
|
||
if ((lbIndex++) % 20 == 0) {
|
||
return updateAllLeaderboards()
|
||
}
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!save'],
|
||
'View your savefile',
|
||
async ({ say, user }) => {
|
||
say('Look, it\'s you! Formatting is ugly because long texts get split up by Slack :[\n```\n' + JSON.stringify(user) + '\n```')
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!as'],
|
||
'Run commands as another user.',
|
||
async ({ event, args, trueSay }) => {
|
||
const [impersonating, ...newWords] = args
|
||
event.user = idFromWord(impersonating)
|
||
await getUser(event.user)
|
||
const isDisabled = users[event.user].isDisabled
|
||
users[event.user].isDisabled = false
|
||
event.text = newWords.join(' ')
|
||
await messageHandler({ event, say: trueSay, isRecycle: false, skipCounting: true })
|
||
users[event.user].isDisabled = isDisabled
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!enable'],
|
||
'Enable the given user',
|
||
async ({ args }) => {
|
||
const user = await getUser(idFromWord(args[0]))
|
||
if (user.isDisabled) {
|
||
user.isDisabled = false
|
||
//saveGame()
|
||
addAchievement(user, 'theOtherSide', slack.messageAdmin)
|
||
await slack.postToTechThermostatChannel(`_${user.name} has returned..._`)
|
||
}
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!disable'],
|
||
'Disable the given user',
|
||
async ({ args }) => {
|
||
const user = await getUser(idFromWord(args[0]))
|
||
user.isDisabled = true
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!g', '!gamble'],
|
||
'Gamble away your HVAC\n' +
|
||
' To use, say \'gamble coin_amount\' or \'!gamble all\'',
|
||
async ({ say, trueSay, args, user, event }) => {
|
||
if (event.text?.includes(`y'all`)) {
|
||
if (event.user !== slack.users.Admin) {
|
||
return say('Perhaps another time...')
|
||
}
|
||
await say(`Gambling the souls of all players...`)
|
||
setTimeout(async () => {
|
||
say('You bet the souls of your coworkers and won 1 HVAC!')
|
||
addCoins(user, 1)
|
||
}, 25000)
|
||
return
|
||
}
|
||
const argText = args.join(' ')
|
||
const requestedWager = parseAll(argText, user.coins, user)
|
||
const n = requestedWager//(chaosFilter(requestedWager, 0.2, user, user.coins) + requestedWager) / 2
|
||
if (!n || n < 0) {
|
||
return say(`Invalid number '${argText}'`)
|
||
}
|
||
if (user.coins < n) {
|
||
return say(`You don't have that many coins! You have ${commas(user.coins)}.`)
|
||
}
|
||
if (n >= 100_000_000_000) {
|
||
addAchievement(user, 'bigBets', say)
|
||
}
|
||
if (n >= 100_000_000_000_000) {
|
||
addAchievement(user, 'hugeBets', say)
|
||
}
|
||
if (n >= 100_000_000_000_000_000) {
|
||
addAchievement(user, 'mondoBets', say)
|
||
}
|
||
if (n >= 100_000_000_000_000_000_000) {
|
||
addAchievement(user, 'sigmaBets', say)
|
||
}
|
||
user.coins -= n
|
||
let outcome
|
||
if (Math.random() > 0.5) {
|
||
user.coins += (2 * n)
|
||
outcome = 'won'
|
||
} else {
|
||
outcome = 'lost'
|
||
}
|
||
console.log(`They ${outcome}`)
|
||
//saveGame()
|
||
await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`)
|
||
if (outcome === 'lost' && user.lostBetMessage) {
|
||
await trueSay(user.lostBetMessage)
|
||
} else if (outcome === 'won' && user.wonBetMessage) {
|
||
await trueSay(user.wonBetMessage)
|
||
}
|
||
return updateAllLeaderboards()
|
||
}
|
||
)
|
||
|
||
const emojiRegex = /^:[^:\s]*:$/
|
||
const validEmoji = async emojiText => {
|
||
emojiText = emojiText?.trim()
|
||
if (!emojiText || !emojiRegex.test(emojiText)) {
|
||
return false
|
||
}
|
||
const validEmojis = (await getEmojis()).emoji
|
||
const noColons = emojiText.replace(/:/g, '')
|
||
|
||
// console.log('validEmojis', validEmojis)
|
||
return !!validEmojis[noColons]
|
||
}
|
||
const getEmojis = async () => await slack.app.client.emoji.list()
|
||
|
||
command(
|
||
['!setloss'],
|
||
'!setloss <emoji>',
|
||
async ({ args, user, say }) => {
|
||
const emoji = args[0]
|
||
if (!await validEmoji(emoji)) {
|
||
return say(`Argument must be a single emoji!`)
|
||
}
|
||
user.lostBetMessage = emoji
|
||
say(`Set!`)
|
||
}, {hidden: true})
|
||
|
||
command(
|
||
['!setwon', '!setwin'],
|
||
'!setwon <emoji>',
|
||
async ({ args, user, say }) => {
|
||
const emoji = args[0]
|
||
if (!await validEmoji(emoji)) {
|
||
return say(`Argument must be a single emoji!`)
|
||
}
|
||
user.wonBetMessage = emoji
|
||
say(`Set!`)
|
||
}, {hidden: true})
|
||
|
||
command(
|
||
['!buynft', '!bn'],
|
||
'Acquire high-quality art\n' +
|
||
' To use, say \'!buynft nft_name\'',
|
||
async ({ event, say, args, user }) => {
|
||
const nft = nfts.find(n => n.name.toLowerCase() === args[0])
|
||
if (!nft) {
|
||
const suffix = args[0]?.match(/[^a-z0-9_]/i) ? '. And I\'m unhackable, so cut it out.' : ''
|
||
return say('No NFT with that name found' + suffix)
|
||
}
|
||
if (nft.owner) {
|
||
return say('Someone already owns that NFT!')
|
||
}
|
||
const c = user.coins
|
||
if (c < nft.price) {
|
||
return say('You don\'t have enough coin for this nft')
|
||
}
|
||
|
||
user.coins -= nft.price
|
||
nft.owner = event.user
|
||
//saveGame()
|
||
await say('You bought ' + nft.name + '!')
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!myupgrades', '!myu'],
|
||
'List all the upgrades that you own.',
|
||
async ({ say, user }) => {
|
||
await say(upgradeText(user, true))
|
||
}, dmsOnly)
|
||
|
||
const upgradeText2 = (user, extraMessage = '') => {
|
||
const userDoesNotHave = ([upgradeName, upgrade]) => !hasUpgrade(user, upgrade, upgradeName)
|
||
const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
|
||
return ({
|
||
text: (extraMessage && extraMessage + '\n') + upgradeText(user, false),
|
||
blocks: [
|
||
(extraMessage && {
|
||
type: 'section',
|
||
text: {
|
||
type: 'mrkdwn',
|
||
text: extraMessage + '\n'
|
||
},
|
||
}),
|
||
...Object.entries(upgrades)
|
||
.filter(userDoesNotHave)
|
||
.filter(userMeetsCondition)
|
||
.map(([upgradeName]) => upgradeBlock(upgradeName))
|
||
].filter(block => block)
|
||
})
|
||
}
|
||
|
||
command(
|
||
['!upgrade', '!u'],
|
||
'Improve the performance of your HVAC-generators.\n' +
|
||
' Say \'!upgrade\' to list available upgrades, or \'!upgrade upgrade_name\' to purchase directly.',
|
||
async ({ say, args, user }) => {
|
||
if (!args[0]) {
|
||
return say(upgradeText2(user))
|
||
}
|
||
console.log({args: args.join(' ')})
|
||
const matcher = fuzzyMatcher(args.join(' '))
|
||
const u = Object.entries(upgrades).find(([name, upgrade]) => matcher.test(name) || matcher.test(upgrade.name))
|
||
if (!u) {
|
||
return say(`Could not find an upgrade matching "${args.join(' ')}"!`)
|
||
}
|
||
const [id, upgrade] = u
|
||
if (!user.upgrades[upgrade.type]) {
|
||
user.upgrades[upgrade.type] = []
|
||
}
|
||
if (hasUpgrade(user, upgrade, id)) {
|
||
return say(`You already have ${upgrade.name}!`)
|
||
}
|
||
if (!upgrade.condition(user, getCompletedSquadgradeNames())) {
|
||
return say('That item does not exist!')
|
||
}
|
||
const c = user.coins
|
||
if (c < upgrade.cost) {
|
||
return say(`You don't have enough coins to buy ${upgrade.name}!\nYou have ${commas(c)}, but you need ${commas(upgrade.cost)}`)
|
||
}
|
||
user.coins -= upgrade.cost
|
||
user.upgrades[upgrade.type].push(id)
|
||
//saveGame()
|
||
await say(`You bought ${id}!`)
|
||
}, dmsOnly)
|
||
|
||
const upgradeBlock = upgradeName => {
|
||
const upgrade = upgrades[upgradeName]
|
||
return ({
|
||
type: 'section',
|
||
text: {
|
||
type: 'mrkdwn',
|
||
text: `${upgrade.name} :${buyableItems[upgrade.type]?.emoji || upgrade.emoji}: - H${commas(upgrade.cost)}\n_${upgrade.description}_`
|
||
},
|
||
accessory: {
|
||
type: 'button',
|
||
text: {
|
||
type: 'plain_text',
|
||
text: 'Buy',
|
||
emoji: true
|
||
},
|
||
value: 'upgrade_' + upgradeName,
|
||
action_id: 'upgrade_' + upgradeName
|
||
}
|
||
})
|
||
}
|
||
|
||
const upgradeButton = async ({ body, ack, say, payload }) => {
|
||
await ack()
|
||
const upgrade = payload.action_id.substring(8)
|
||
console.log(`upgradeButton ${upgrade} clicked`)
|
||
const event = {
|
||
user: body.user.id
|
||
}
|
||
const user = await getUser(event.user, true)
|
||
const words = ['!upgrade', upgrade]
|
||
const [commandName, ...args] = words
|
||
let extraMessage = ''
|
||
say = async text => extraMessage = text
|
||
await commands.get('!u').action({ 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,
|
||
...upgradeText2(user, extraMessage)
|
||
})
|
||
await updateAllLeaderboards()
|
||
}
|
||
|
||
Object.keys(upgrades).forEach(upgradeName => slack.app.action('upgrade_' + upgradeName, upgradeButton))
|
||
|
||
const getCurrentSquadgrade = () => {
|
||
const current = Object.entries(squadUpgrades).find(squadIsMissing)
|
||
if (!current) {
|
||
return current
|
||
}
|
||
const [name, upgrade] = current
|
||
if (!squad.upgrades[name]) {
|
||
squad.upgrades[name] = upgrade.cost
|
||
}
|
||
return {
|
||
name,
|
||
upgrade,
|
||
remaining: squad.upgrades[name],
|
||
emoji: upgrade.emoji,
|
||
description: upgrade.description
|
||
}
|
||
}
|
||
|
||
const squadText = () => {
|
||
const current = getCurrentSquadgrade()
|
||
if (current) {
|
||
const currentUpgradeText = ({ name, remaining, emoji, description }) =>
|
||
`:${emoji}: *${name}* - ${commas(remaining)} HVAC remaining.\n_${description}_`
|
||
return currentUpgradeText(current)
|
||
}
|
||
return 'All squadgrades are unlocked!\n\n' + Object.values(squadUpgrades).map(({ name, cost, emoji, description }) =>
|
||
`:${emoji}: *${name}* - ${commas(cost)} HVAC.\n_${description}_`).join('\n\n')
|
||
}
|
||
|
||
command(
|
||
['!cat'],
|
||
'View your total all-time coins collected',
|
||
async ({ say, user }) => {
|
||
await say(`You've earned a total of ${commas(user.coinsAllTime)} HVAC`)
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!squad', '!sq'],
|
||
'Buy upgrades that help the whole team.\n' +
|
||
' Say !squad to list squad upgrades.\n' +
|
||
' Say \'!squad contrib_amount\' to make progress toward the current upgrade.',
|
||
async ({ say, args, user }) => {
|
||
if (!args[0]) {
|
||
return say(squadText())
|
||
}
|
||
const current = getCurrentSquadgrade()
|
||
if (!current) {
|
||
return say('No squadgrades are currently available')
|
||
}
|
||
const currentCoins = user.coins
|
||
let amount = parseAll(args.join(' '), currentCoins, user)
|
||
if (amount > currentCoins) {
|
||
return say(`You don't have that much HVAC! You have ${currentCoins}.`)
|
||
}
|
||
if (!amount || amount < 1) {
|
||
return say(`Invalid amount: '${args[0]}'`)
|
||
}
|
||
if (amount > squad.upgrades[current.name]) {
|
||
amount = squad.upgrades[current.name]
|
||
}
|
||
squad.upgrades[current.name] -= amount
|
||
user.coins -= amount
|
||
user.squadGradeContributions ??= 0
|
||
user.squadGradeContributions += amount
|
||
let status
|
||
if (squad.upgrades[current.name] < 1) {
|
||
squad.upgrades[current.name] = true
|
||
status = `\n\nYou now have ${current.name}!`
|
||
} else {
|
||
status = ` Current status:\n\n${squadText()}`
|
||
}
|
||
if (user.squadGradeContributions > 10_000_000_000_000_000) {
|
||
//addAchievement(user, '')
|
||
}
|
||
//saveGame()
|
||
await say(`Thank you for your contribution of ${commas(amount)} HVAC!${status}`)
|
||
}
|
||
)
|
||
|
||
const { buyRoute, leaderboardUpdater } = require('./buy')
|
||
command(
|
||
['!buy', '!b', '?b', '?buy'],
|
||
'Buy new items to earn HVAC with\n' +
|
||
' Use without arguments to list all available items.\n' +
|
||
' Say \'!buy item_name optional_quantity\' to make your purchase.\n' +
|
||
' Say \'?b item_name optional_quantity\' to check how much your purchase will cost.',
|
||
buyRoute
|
||
)
|
||
|
||
command(
|
||
['!changelog', '!changes'],
|
||
`View my current git log`,
|
||
async ({ event, }) => {
|
||
const command = `git log > ./gitlog`
|
||
const child = exec(command)
|
||
child.on('close', async () => {
|
||
await slack.app.client.files.upload({
|
||
channels: event.channel,
|
||
initial_comment: 'Here\'s my current `git log`',
|
||
file: createReadStream('./gitlog')
|
||
})
|
||
})
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!check', '!ch', '!ᴄheck', '!ᴄh'],
|
||
'Check how many coins another player has',
|
||
async ({ say, args, event }) => {
|
||
const targetId = idFromWord(args[0])
|
||
if (!targetId) {
|
||
return say('Target must be a valid @')
|
||
}
|
||
|
||
if (targetId === slack.users.Hvacker) {
|
||
const members = (await slack.app.client.conversations.members({ channel: slack.temperatureChannelId })).members
|
||
const humanMembers = members.filter(name => name.length === 11)
|
||
return say(`Hvacker owns ${humanMembers.length} souls.`)
|
||
}
|
||
const user = await getUser(targetId)
|
||
if (user.isDisabled) {
|
||
return say(`<@${targetId}> is no longer with us.`)
|
||
}
|
||
const fakeC = 'ᴄ'
|
||
const coins = event.text[1] === fakeC ? 0 : getCoins(targetId)
|
||
await say(`<@${targetId}> has ${commas(coins, args[1] === 'exact')} HVAC.`)
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!gift', '!give', '!gi'],
|
||
'Donate coins to a fellow player\n' +
|
||
' Send coins by saying \'!gift @player coin_amount\'',
|
||
async ({ event, args, say, user, haunted }) => {
|
||
return say(`I'm sorry, but you people can't be trusted anymore.`)
|
||
if (haunted) {
|
||
return say(`!give doesn't work while you're haunted.`)
|
||
}
|
||
let [target, ...amountText] = args
|
||
amountText = amountText.join(' ')
|
||
let targets
|
||
if (target === 'everyone') {
|
||
targets = Object.entries(users).filter(([id, user]) => id !== event.user && !user.isDisabled && user.name && user.name !== 'Hvacker').map(([id]) => id)
|
||
} else {
|
||
const targetId = idFromWord(target)
|
||
targets = [targetId]
|
||
if (targetId === event?.user) {
|
||
return say(':thonk:')
|
||
}
|
||
if (!targetId) {
|
||
return say('Target must be a valid @')
|
||
}
|
||
if (amountText === 'all' && slack.users.Tyler === targetId) {
|
||
addAchievement(user, 'walmartGiftCard', say)
|
||
}
|
||
}
|
||
let individualAmount = parseAll(amountText, user.coins, user)
|
||
if (individualAmount === user.coins) {
|
||
individualAmount = user.coins / targets.length
|
||
}
|
||
const totalAmount = individualAmount * targets.length
|
||
|
||
if (!totalAmount || totalAmount < 0) {
|
||
return say('Amount must be a positive integer!')
|
||
}
|
||
if (user.coins < totalAmount) {
|
||
return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`)
|
||
}
|
||
let gifted = []
|
||
for (const targetId of targets) {
|
||
const targetUser = await getUser(targetId)
|
||
user.coins -= individualAmount
|
||
if (user.coinsAllTime < 10000) {
|
||
// return say('Let \'em play for a bit, ay?')
|
||
continue
|
||
}
|
||
gifted.push(targetId)
|
||
targetUser.coins += individualAmount
|
||
}
|
||
let recipients
|
||
if (gifted.length > 1) {
|
||
const last = gifted.pop()
|
||
recipients = gifted.map(t => users[t].name).join(', ') + ', and ' + users[last].name
|
||
} else {
|
||
console.log('gifted', gifted)
|
||
console.log('users[gifted[0]]', users[gifted[0]])
|
||
recipients = users[gifted[0]].name
|
||
console.log('recipients', recipients)
|
||
}
|
||
await say(`Gifted ${commas(individualAmount)} HVAC to ${recipients}`)
|
||
}
|
||
)
|
||
|
||
const getChaosMessage = (user, { channel_type }, prefix = '', postfix = '') => {
|
||
const userQuackgrades = user.quackUpgrades?.cps || []
|
||
if (userQuackgrades.includes('chaos') && channel_type?.endsWith('im')) {
|
||
return `${prefix}Current chaos multiplier: ${getChaos(user)}${postfix}`
|
||
}
|
||
return ''
|
||
}
|
||
|
||
command(
|
||
['!status', '!s'],
|
||
'Print your current CPS, HVAC balance, and owned items',
|
||
async ({ event, say, user }) => {
|
||
await say(
|
||
`You are currently earning \`${commas(getCPS(user))}\` HVAC Coin per second.\n\n` +
|
||
getChaosMessage(user, event, '', '\n\n') +
|
||
`You currently have ${commas(user.coins)} HVAC Coins\n\n` +
|
||
`${collection(user)}\n\nCoins collected all-time: ${commas(user.coinsAllTime)}\n\n`
|
||
)
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!gimme'],
|
||
'Give self x coins',
|
||
async ({ say, args, user }) => {
|
||
const increase = parseAll(args.join(' '))
|
||
addCoins(user, increase)
|
||
await say(`You now have ${user.coins} HVAC.`)
|
||
}, testOnly)
|
||
|
||
command(
|
||
['!nfts', '!nft', '!n'],
|
||
'Show NFTs in the museum\n' +
|
||
' Call with no arguments to list all NFTs, or \'!nft nft_name\' to show a particular one.',
|
||
async ({ say, args }) => {
|
||
const owner = nft => `Owner: *${slack.users[nft.owner] || 'NONE'}*`
|
||
const nftDisplay = nft => `_"${nft.name}"_\n\n${nft.description}\n\n${commas(nft.price)} HVAC.\n\n${nft.picture}\n\n${owner(nft)}`
|
||
|
||
const matcher = fuzzyMatcher(args[0] || '')
|
||
|
||
await say(nfts
|
||
.filter(({name}) => matcher.test(name))
|
||
.map(nftDisplay)
|
||
.join('\n-------------------------\n') || (args[0] ? 'No matching NFTs found' : 'No NFTs currently exist.')
|
||
)
|
||
}
|
||
)
|
||
|
||
let emojiLevel = 1
|
||
const buildPEmoji = name => {
|
||
const ret = [name, emojiLevel]
|
||
emojiLevel *= 2
|
||
return ret
|
||
}
|
||
const prestigeEmojis = [
|
||
'rock',
|
||
'wood',
|
||
'seedling',
|
||
'evergreen_tree',
|
||
'hibiscus',
|
||
'thunder_cloud_and_rain',
|
||
'rainbow',
|
||
'star',
|
||
'dizzy',
|
||
'sparkles',
|
||
'star2',
|
||
'stars',
|
||
'comet',
|
||
'night_with_stars',
|
||
'milky_way',
|
||
'eye',
|
||
].map(buildPEmoji)
|
||
|
||
const prestigeEmoji = user => {
|
||
const p = user.prestige || 0
|
||
if (!p) {
|
||
return ''
|
||
}
|
||
let e = ''
|
||
for (const [emoji, requiredLevel] of prestigeEmojis) {
|
||
if (p < requiredLevel) {
|
||
break
|
||
}
|
||
e = emoji
|
||
}
|
||
return e && `:${e}:`
|
||
}
|
||
|
||
command(
|
||
['!pemojis', '!pemoji'],
|
||
`Show the emoji for each prestige level you've been through.`,
|
||
async ({ say, user, event, args }) => {
|
||
let p = user.prestige || 0
|
||
if (event.user === slack.users.Admin && args[0] === 'all') {
|
||
p = 99999999
|
||
}
|
||
let message = ''
|
||
for (const [emoji, requiredLevel] of prestigeEmojis) {
|
||
if (requiredLevel > p) {
|
||
break
|
||
}
|
||
message += `${requiredLevel} => :${emoji}:\n`
|
||
}
|
||
return say(message)
|
||
}, prestigeOnly)
|
||
|
||
command(
|
||
['!pet'],
|
||
`Take care of the office pet!\nPet bonuses are shared between all players!`,
|
||
async ({ say, user, event, args }) => {
|
||
pets.petToText(pet, null, say)
|
||
})
|
||
|
||
const strike = user => user.isDisabled ? '~' : ''
|
||
const struck = (user, string) => strike(user) + string + strike(user)
|
||
|
||
const generateLeaderboard = ({ args }, showCat) => {
|
||
let index = 1
|
||
return Object.entries(users)
|
||
.filter(([, user]) => (!user.isDisabled || args.includes('all')) && user.name && !['Hvacker', '???', 'TEST-USER'].includes(user.name))
|
||
.sort(([id, user1], [id2, user2]) => {
|
||
const leftPrestige = getUserSync(id).prestige
|
||
const rightPrestige = getUserSync(id2).prestige
|
||
if (leftPrestige > rightPrestige) {
|
||
return -1
|
||
}
|
||
if (rightPrestige > leftPrestige) {
|
||
return 1
|
||
}
|
||
return getCPS(user1) > getCPS(user2)
|
||
})
|
||
.map(([id, u]) => `${strike(u)}${index++}. ${slack.users[id] || '???'} ${prestigeEmoji(u) || '-'} ${commas(getCPS(getUserSync(id)))} CPS - ${commas(getCoins(id))} HVAC${showCat ? ` (${commas(users[id].coinsAllTime)} all-time)` : ''}${strike(u)}`)
|
||
.join('\n')
|
||
}
|
||
|
||
command(
|
||
['!leaderboard', '!lb'],
|
||
'Show the top HVAC-earners, ranked by prestige, then CPS',
|
||
async ({ say, user, args, event }) => {
|
||
// if ((event.user === slack.users.Houston || event.user === slack.users.Admin) && event.channel_type.includes('im')) {
|
||
// return say('```' + `Hvacker - 9 souls - Taking from them whatever it desires\nSome other losers - who cares - whatever` + '```')
|
||
// }
|
||
game.leaderboardChannels ??= {}
|
||
const showCat = event.user === slack.users.Admin && args.includes('cat')
|
||
await say(generateLeaderboard({ args }, showCat)).then(({ channel, ts }) => {
|
||
addAchievement(user, 'leaderBoardViewer', say)
|
||
if (args.includes('all') || showCat) {
|
||
return
|
||
}
|
||
game.leaderboardChannels[channel] = ts
|
||
return updateAllLeaderboards({ channel, ts })
|
||
})
|
||
}
|
||
)
|
||
|
||
const updateAllLeaderboards = async (add) => {
|
||
const currentBoard = generateLeaderboard({ args: [] })
|
||
return updateAll({ name: 'leaderboard', text: currentBoard, add })
|
||
}
|
||
|
||
leaderboardUpdater.updateAllLeaderboards = updateAllLeaderboards
|
||
|
||
const hints = []
|
||
const oneShot = (name, helpText, message, achievementName) => {
|
||
if (helpText) {
|
||
hints.push(helpText)
|
||
}
|
||
const names = Array.isArray(name) ? name : [name]
|
||
|
||
command(names, helpText, async ({ say, user }) => {
|
||
await say(message)
|
||
if (achievementName) {
|
||
addAchievement(user, achievementName, say)
|
||
}
|
||
}, { hidden: true })
|
||
}
|
||
|
||
const oneShotFile = async (name, helpText, text, filePath, achievementName) => {
|
||
const names = Array.isArray(name) ? name : [name]
|
||
command(names, helpText, async ({ event }) => {
|
||
await slack.app.client.files.upload({
|
||
channels: event.channel,
|
||
initial_comment: text,
|
||
file: createReadStream(filePath)
|
||
})
|
||
if (achievementName) {
|
||
addAchievement(user, achievementName, say)
|
||
}
|
||
}, { hidden: true })
|
||
}
|
||
|
||
oneShot('!peter-griffin-family-guy', 'Good stuff great comedy.', `Are you sure?\nThis will permanently delete all of your progress and remove you from the game.\nSay !!peter-griffin-family-guy to confirm.`)
|
||
oneShotFile('!santa', 'Ho ho ho!', '<https://i.imgur.com/dBWgFfQ.png|I\'m Santa Quacks>')
|
||
oneShot('!sugma', 'Not very original.', ':hvacker_angery:')
|
||
oneShotFile('!pog', 'One poggers hvacker', 'poggers', 'images/XCg7WDz.png')
|
||
oneShotFile('!ligma', 'Not very original.', 'no', 'images/i1YtW7m.png')
|
||
oneShotFile(['!dab', '!dabs'], 'ACTIVATE COOL GUY MODE', 'I go XD style', 'images/FKYdeqo.jpg', 'certifiedCoolGuy')
|
||
oneShotFile('!based', 'Sorry, it\'s a little hard to hear you!', 'What?', 'images/IUX6R26.png')
|
||
oneShotFile('!shrek', 'Is love and is life.', 'Donkey!', 'images/QwuCQZA.png')
|
||
oneShotFile('!sugondese', 'I don\'t like you.', 'rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr', 'images/VCvfvdz.png')
|
||
oneShot('!bofa', 'bofa deez yes yes we get it', ':goorab:')
|
||
oneShotFile('!squeedward', 'Who lives in an easter island head, under the sea?', 'Squeedward', 'images/squeedward.png')
|
||
// oneShot('!imaginedragons', 'The worst part about any group of white people is that they could be Imagine Dragons and you\'d have no way of knowing.', '<https://i.imgur.com/QwuCQZA.png|Donkey!>')
|
||
|
||
command(
|
||
['!hint'],
|
||
'',
|
||
async ({ say }) => say(`_${hints[Math.floor(Math.random() * hints.length)]}_`)
|
||
, {hidden: true})
|
||
|
||
command(
|
||
['!addnft'],
|
||
`Arguments 1 and 2 should be on the first line as name (one word!) and integer price.
|
||
The second line should be the description of the pieces
|
||
the picture is everything after the second line`,
|
||
async ({ event }) => {
|
||
let [, name, ...price] = event.text.substring(0, event.text.indexOf('\n')).split(' ')
|
||
const rest = event.text.substring(event.text.indexOf('\n') + 1)
|
||
const desc = rest.substring(0, rest.indexOf('\n'))
|
||
const picture = rest.substring(rest.indexOf('\n') + 1)
|
||
price = price.join(' ')
|
||
const newNft = {
|
||
name,
|
||
price: parseInt(price.replace(/,/g, '')),
|
||
description: desc,
|
||
picture,
|
||
owner: null
|
||
}
|
||
nfts.push(newNft)
|
||
console.log('addedNft', newNft)
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!prestige', '!p'],
|
||
'Show your current prestige status',
|
||
prestige.prestigeRoute
|
||
)
|
||
|
||
command(
|
||
['!!prestige'],
|
||
'Confirm your prestige activation.',
|
||
prestige.prestigeConfirmRoute, { hidden: true })
|
||
|
||
command(
|
||
['!pbeta'],
|
||
'Confirm your prestige activation.',
|
||
async ({ event, say, user, YEET }) => {
|
||
await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
|
||
}, { hidden: true })
|
||
|
||
command(
|
||
['!quack', '!quackstore'],
|
||
'Confirm your prestige activation.',
|
||
async ({ event, say, user, YEET }) => {
|
||
await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
|
||
}, prestigeOnly)
|
||
|
||
// command(
|
||
// ['!quack', '!quackstore'],
|
||
// 'Spend your prestigious quackings\n\n' +
|
||
// 'Say \'!quack upgrade_name\' to purchase a quackgrade',
|
||
// prestige.quackStoreRoute,
|
||
// prestigeOnly
|
||
// )
|
||
|
||
command(
|
||
['!myquack', '!myq'],
|
||
'See what kind of quackery you own.',
|
||
prestige.ownedQuacksRoute,
|
||
prestigeOnly
|
||
)
|
||
|
||
command(
|
||
['!lore', '!l'],
|
||
'Sit down for a while. Maybe learn something.\n' +
|
||
'You can use `!lore reset` to return to the beginning.',
|
||
lore,
|
||
dmsOnly
|
||
)
|
||
|
||
command(
|
||
['!ss'],
|
||
'Show the status for another player: !ss @player',
|
||
async ({ args, say }) => {
|
||
const target = args[0]
|
||
const targetId = idFromWord(target)
|
||
const targetUser = await getUser(targetId)
|
||
await say(
|
||
`${target} is currently earning \`${commas(getCPS(targetUser))}\` HVAC Coin per second.\n\n` +
|
||
`They have ${commas(getCoins(targetId))} HVAC Coins\n\n` +
|
||
`${collection(targetUser)}\n\n` +
|
||
`${Object.entries(targetUser?.items || {}).reduce((total, [, count]) => total + count, 0)} total items\n\n`
|
||
)
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!steal', '!sagesteal'],
|
||
'Highly illegal',
|
||
async ({ event, args, say, user }) => {
|
||
const [target] = args
|
||
const amount = user.coins / 100
|
||
if (!amount || amount < 0) {
|
||
return
|
||
}
|
||
let targetId = idFromWord(target)
|
||
console.log({ user: event.user, target, targetId })
|
||
if (event.user === targetId) {
|
||
return say('What, are you trying to steal from yourself? What, are you stupid?')
|
||
}
|
||
if (!targetId) {
|
||
targetId = slack.users.Admin
|
||
}
|
||
if (user.coins < amount) {
|
||
return
|
||
}
|
||
const targetUser = await getUser(targetId)
|
||
user.coins -= amount
|
||
targetUser.coins += amount
|
||
await say(`Stealing is wrong. Gave ${commas(amount)} of your HVAC to ${slack.users[targetId]}`)
|
||
}, { hidden: true })
|
||
|
||
command(
|
||
['!lotto'],
|
||
'Once per day, try for a big win!',
|
||
async ({ event, say, user }) => {
|
||
const currentDate = new Date().getDate()
|
||
const lastLotto = user.lastLotto === undefined ? currentDate - 1 : user.lastLotto
|
||
if (lastLotto === currentDate && event.user !== slack.users.Admin) {
|
||
return say('Hey, only one lotto ticket per day, alright?')
|
||
}
|
||
let msg
|
||
if (Math.random() < 0.01) {
|
||
const prize = 5000 + (user.coinsAllTime / 20)
|
||
msg = `Ayyyyy, we have a winner! You win ${commas(prize)} HVAC!`
|
||
addCoins(user, prize)
|
||
} else {
|
||
msg = `Sorry pal, today's not your lucky day.`
|
||
}
|
||
await say(msg)
|
||
user.lastLotto = currentDate
|
||
//saveGame()
|
||
}, { hidden: true })
|
||
|
||
command(
|
||
['!ignite'],
|
||
'You found me!',
|
||
async ({ say, user }) => {
|
||
addAchievement(user, 'ignited', say)
|
||
}, { hidden: true })
|
||
|
||
command(
|
||
['!giveach'],
|
||
'!giveach @player ach_name',
|
||
async ({ args, say, }) => {
|
||
addAchievement(await getUser(idFromWord(args[0])), args[1], say)
|
||
//saveGame()
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!whois'],
|
||
'!whois player_id',
|
||
async ({ args, say }) => say(`<@${args[0]}>`),
|
||
adminOnly)
|
||
|
||
command(
|
||
['!!postas'],
|
||
'',
|
||
async ({ args }) => {
|
||
let channel = slack.temperatureChannelId
|
||
if (args[0].startsWith('<#')) {
|
||
channel = args[0].substring(2, args[0].indexOf('|'))
|
||
const [, ...rest] = args
|
||
args = rest
|
||
}
|
||
if (args[0].startsWith('<@')) {
|
||
channel = args[0].substring(2, args[0].length - 1)
|
||
const [, ...rest] = args
|
||
args = rest
|
||
}
|
||
|
||
console.log({args, channel})
|
||
const target = idFromWord(args[0])
|
||
const [, ...rest] = args
|
||
const userInfo = await slack.app.client.users.info({
|
||
user: target
|
||
})
|
||
console.log(userInfo)
|
||
return slack.app.client.chat.postMessage({
|
||
channel,
|
||
text: rest.join(' '),
|
||
username: userInfo.user.profile.real_name,
|
||
icon_url: userInfo.user.profile.image_original
|
||
})
|
||
},
|
||
adminOnly)
|
||
|
||
command(
|
||
['!!startpoll'],
|
||
'',
|
||
async ({ args, say }) => {
|
||
},
|
||
adminOnly)
|
||
|
||
command(
|
||
['!ngift'],
|
||
'!ngift player_id nft_name',
|
||
async ({ event, args, say, haunted }) => {
|
||
if (haunted) {
|
||
return say(`!ngift doesn't work while you're haunted.`)
|
||
}
|
||
const targetId = idFromWord(args[0])
|
||
if (!targetId) {
|
||
return say('Please specify a valid @ target!')
|
||
}
|
||
const nft = nfts.find(nft => nft.name === args[1])
|
||
if (!nft) {
|
||
return say(`There is not NFT named "${args[1]}"!`)
|
||
}
|
||
if (nft.owner !== event.user) {
|
||
return say(`You do not own "${nft.name}"!`)
|
||
}
|
||
nft.owner = targetId
|
||
}, { hidden: true })
|
||
|
||
command(
|
||
['!deletetest', '!dtest'],
|
||
'!deletetest',
|
||
async () => {
|
||
delete users[slack.testId]
|
||
//saveGame()
|
||
}, adminOnly)
|
||
|
||
const stonkPatterns = {
|
||
// Change %: ~1.25
|
||
duk: [
|
||
2.36, -1.69, -1.93, 4.95, -0.99, -0.89,
|
||
0.90, 2.21, 1.29, -4.93, 1.90, 0.90,
|
||
0.37, -0.07, 2.85, 0.25, 0.40, 1.58,
|
||
3.10, 1.37, 0.68, -2.57, -1.80, -0.21,
|
||
-2.80, -0.11, 0.31, -0.25, 1.56, 1.97,
|
||
-0.44, -6.28, 2.67, 2.85, 5.37, 2.04,
|
||
3.01, 1.75, -3.53, 0.38, -7.63, -1.89,
|
||
-0.83, -2.27, 6.14, -2.05, 0.84, -3.22,
|
||
2.34, -1.79, 1.26, -2.48, -5.73, 0.37,
|
||
-4.63, 5.31, 1.23
|
||
],
|
||
// Change %: ~13.30
|
||
quak: [
|
||
3.21, -0.81, 0.84, 0.17, 2.18, -0.19,
|
||
-2.57, 1.26, 0.44, -2.28, 0.11, -0.21,
|
||
1.16, 4.07, -0.28, 0.61, 1.76, -0.90,
|
||
1.14, -2.37, 0.96, -2.35, -3.42, -1.21,
|
||
0.00, 2.10, -0.18, 3.42, -1.71, -0.32,
|
||
-1.77, -1.46, 0.87, 0.60, 1.63, 1.51,
|
||
-0.07, 1.87, -0.38, -0.44, -1.02, 1.70,
|
||
-0.46, -4.32, 0.06, 0.41, 5.03, 0.84,
|
||
-1.03, 3.88, 3.38, 2.24, -0.43, -0.50,
|
||
-3.61, 0.32
|
||
],
|
||
// Change %: ~0.77
|
||
honk: [
|
||
-0.30, 3.73, 11.27, -7.71, -6.81, 1.15,
|
||
3.55, -3.42, 8.51, -3.22, 8.20, 0.19,
|
||
0.76, -2.00, 9.63, 0.87, 3.14, -3.76,
|
||
-2.27, 3.42, 2.39, 4.51, -0.35, -0.95,
|
||
-6.64, -6.88, 8.90, 0.42, -0.04, -3.33,
|
||
1.85, 4.16, -5.26, -7.24, 5.35, 0.46,
|
||
5.16, -0.10, -5.24, 5.34, -8.52, 6.17,
|
||
-0.80, 2.92, -2.21, -6.80, -2.22, 10.16,
|
||
-1.63, -10.11, 6.88, -4.19, -8.53, -2.68,
|
||
-7.49, 5.70, 1.23, -1.0
|
||
],
|
||
}
|
||
|
||
const nextPattern = pattern => {
|
||
switch (pattern) {
|
||
case "duk":
|
||
return "quak"
|
||
case "quak":
|
||
return "hvac"
|
||
default:
|
||
return "duk"
|
||
}
|
||
}
|
||
|
||
const updateStonkPrices = () => {
|
||
const today = daysSinceEpoch()
|
||
if (stonkMarket.lastDay === today) {
|
||
return // already updated
|
||
}
|
||
|
||
// TODO: Gotta take into account wrapping around to the end of the year
|
||
Object.entries(stonkMarket.stonks).forEach(([, stonk]) => {
|
||
console.log(stonk.pattern)
|
||
console.log('try set')
|
||
for (let i = stonkMarket.lastDay; i < today; i++) {
|
||
console.log('set lastPrice')
|
||
stonk.lastPrice = stonk.price
|
||
console.log(stonk.pattern, stonkPatterns)
|
||
stonk.price *= 1 + ((stonkPatterns[stonk.pattern] || stonkPatterns.duk)[stonk.index] / 100)
|
||
stonk.index++
|
||
if (stonk.index >= stonkPatterns[stonk.pattern]?.length) {
|
||
stonk.index = 0
|
||
stonk.pattern = nextPattern(stonk.pattern)
|
||
}
|
||
}
|
||
})
|
||
stonkMarket.lastDay = today
|
||
//saveGame(null, true)
|
||
}
|
||
|
||
const buyStonks = (user, stonkName, quantityPhrase) => {
|
||
const quantity = parseAll(quantityPhrase, Math.floor(user.coins / stonkMarket.stonks[stonkName].price), user)
|
||
if (!quantity) {
|
||
return 'Quantity must be positive integer!'
|
||
}
|
||
const cost = stonkMarket.stonks[stonkName].price * quantity
|
||
if (cost > user.coins) {
|
||
return `Buying ${commas(quantity)} of ${stonkName.toUpperCase()} would cost ${commas(cost)} HVAC. You only have ${commas(user.coins)}!`
|
||
}
|
||
user.coins -= cost
|
||
user.holdings ??= {}
|
||
user.holdings[stonkName] ??= 0
|
||
user.holdings[stonkName] += quantity
|
||
//saveGame()
|
||
return `Successfully bought ${commas(quantity)} ${stonkName.toUpperCase()}, for a total of ${commas(cost)} HVAC!`
|
||
}
|
||
|
||
const sellStonks = (user, stonkName, quantityPhrase) => {
|
||
user.holdings ??= {}
|
||
user.holdings[stonkName] ??= 0
|
||
const quantity = parseAll(quantityPhrase, user.holdings[stonkName], user)
|
||
if (!quantity) {
|
||
return 'Quantity must be positive integer!'
|
||
}
|
||
if (quantity > user.holdings[stonkName]) {
|
||
return `You're trying to sell ${commas(quantity)} ${stonkName.toUpperCase()}, but you only have ${user.holdings[stonkName]}!`
|
||
}
|
||
const sellValue = stonkMarket.stonks[stonkName].price * quantity
|
||
user.holdings[stonkName] -= quantity
|
||
user.coins += sellValue
|
||
//saveGame()
|
||
return `You successfully sold ${commas(quantity)} ${stonkName.toUpperCase()}, for a total of ${commas(sellValue)} HVAC!`
|
||
}
|
||
|
||
const stonkHelp =
|
||
'Play the stonk market. Prices change every day!\n' +
|
||
'`!stonk buy <name> <quantity>`\n' +
|
||
'`!stonk sell <name> <quantity>`'
|
||
command(
|
||
['!stonks', '!stonk', '!st'],
|
||
stonkHelp,
|
||
async ({ user, args, say }) => {
|
||
return say(`Stonks are busted atm but thank you for trying!`)
|
||
updateStonkPrices()
|
||
let msg = `Market values:\n`
|
||
Object.entries(stonkMarket.stonks).forEach(([name, stonk]) => {
|
||
const diff = stonk.price - stonk.lastPrice
|
||
const diffPercent = diff / stonk.lastPrice
|
||
const diffSign = diff > 0 ? '+' : ''
|
||
msg += `\`${name.toUpperCase()}\` ${commas(stonk.price)} (${diffSign}${diffPercent.toPrecision(2)}%)\n`
|
||
})
|
||
let [action, stonkName, ...quantityPhrase] = args
|
||
const noHoldingsMessage = msg + `\nYou don't have any holdings right now.`
|
||
if (!action) {
|
||
if (!user.holdings) {
|
||
return say(noHoldingsMessage)
|
||
}
|
||
msg += `\nYou have:`
|
||
let hasHoldings = false
|
||
Object.entries(user.holdings).forEach(([name, holdings]) => {
|
||
if (holdings > 0) {
|
||
hasHoldings = true
|
||
msg += `\n${commas(holdings)} ${name.toUpperCase()} - ${commas(stonkMarket.stonks[name].price * holdings)} HVAC`
|
||
}
|
||
})
|
||
if (!hasHoldings) {
|
||
return say(noHoldingsMessage)
|
||
}
|
||
return say(msg)
|
||
}
|
||
|
||
action = action.toLowerCase()
|
||
stonkName = stonkName.toLowerCase()
|
||
quantityPhrase = quantityPhrase?.join(' ') || '1'
|
||
if (action === 'buy' || action === 'b') {
|
||
return say(buyStonks(user, stonkName, quantityPhrase))
|
||
} else if (action === 'sell' || action === 's') {
|
||
return say(sellStonks(user, stonkName, quantityPhrase))
|
||
} else {
|
||
return say(stonkHelp)
|
||
}
|
||
}
|
||
)
|
||
|
||
command(
|
||
['!speak'],
|
||
'!speak <64-character message>',
|
||
async ({ event, say, user }) => {
|
||
const today = daysSinceEpoch()
|
||
if (user.lastSpeech === today) {
|
||
return say(`Sorry, one speech per day, kid.`)
|
||
}
|
||
if (event.text.length > 64 + '!speak '.length) {
|
||
return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
|
||
}
|
||
if (event.text.length > 64 + '!speak '.length) {
|
||
return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
|
||
}
|
||
if (event.text.includes('<@')) {
|
||
return say(`Please don't @ people when using my voice, you're just gonna get me in trouble.`)
|
||
}
|
||
user.lastSpeech = today
|
||
const message = event.text.substring(7)
|
||
await slack.postToTechThermostatChannel(message)
|
||
//saveGame(null, true)
|
||
}, {
|
||
hidden: true,
|
||
condition: ({ user }) => userHasCheckedQuackgrade(user, 'theVoice')
|
||
})
|
||
|
||
command(
|
||
['!message', '!msg', '!m'],
|
||
'!message player_id message',
|
||
async ({ event, args, say }) => {
|
||
const targetId = idFromWord(args[0])
|
||
if (!targetId) {
|
||
return say('Please specify a valid @ target!')
|
||
}
|
||
const msg = event.text.replace(/\S+ +\S+\s*/i, '')
|
||
await slack.messageIn(targetId, msg)
|
||
}, adminOnly)
|
||
|
||
command(
|
||
['!userjson'],
|
||
'Fetch the raw JSON data for your user',
|
||
async ({ user, trueSay }) => trueSay('```\n' + JSON.stringify(user) + '\n```'),
|
||
{ hidden: true }
|
||
)
|
||
|
||
command(
|
||
['!whohas'],
|
||
'!whohas <achievement name>',
|
||
async ({ user, say, args }) => {
|
||
const achName = args.join(' ')
|
||
const matcher = fuzzyMatcher(achName)
|
||
const achievement = Object.entries(achievements)
|
||
.find(([name, ach]) => [name, ach.name].some(n => matcher.test(n)))
|
||
if (!achievement || !user.achievements[achievement[0]]) {
|
||
return say(`You don't have any achievement matching '${achName}'`)
|
||
}
|
||
const [achId, ach] = achievement
|
||
const owners = Object.entries(users)
|
||
.map(([, potentialOwner]) => potentialOwner)
|
||
.filter(potentialOwner => potentialOwner.achievements[achId])
|
||
.map(owner => owner.name)
|
||
return say(`${owners.length} ${owners.length === 1 ? 'user has' : 'users have'} _${ach.name}_ :${ach.emoji}:\n${owners.join('\n')}`)
|
||
},
|
||
{ hidden: true }
|
||
)
|
||
|
||
const manageReactions = async ({ args, action }) => {
|
||
const [link, ...reactions] = args
|
||
const split = link.replace(/.*archives\//, "").split("/")
|
||
const channelId = split[0]
|
||
// split[1] ~= 'p1234567890123456'
|
||
const t = split[1].substring(1).replace(/[^0-9]/g, "")
|
||
const timestamp = t.substring(0, t.length - 6) + '.' + t.substring(t.length - 6)
|
||
return action({
|
||
app: slack.app,
|
||
channelId,
|
||
timestamp,
|
||
reactions: reactions.map(react => react.replace(/:/gi, ""))
|
||
})
|
||
}
|
||
|
||
command(
|
||
['!react', '!addreact'],
|
||
'!addreact <link> <emojis>',
|
||
async ({ args }) => {
|
||
return manageReactions({ args, action: addReactions })
|
||
},
|
||
adminOnly
|
||
)
|
||
|
||
command(
|
||
['!rmreact'],
|
||
'!rmreact <link> <emojis>',
|
||
async ({ args }) => {
|
||
return manageReactions({ args, action: removeReactions })
|
||
},
|
||
adminOnly
|
||
)
|
||
|
||
webapi.launch()
|
||
logMemoryUsage()
|
||
|
||
module.exports = {
|
||
command,
|
||
adminOnly
|
||
}
|