Init commit
This commit is contained in:
commit
a3c838438e
|
@ -0,0 +1,131 @@
|
||||||
|
.idea/
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
|
@ -0,0 +1 @@
|
||||||
|
{"users":{"U028BMEBWBV":{"coins":145479654964,"lastCheck":1646324260.972,"items":{"mouse":98,"accountant":65,"whale":44,"train":34,"fire":31,"boomerang":25,"moon":17,"butterfly":10,"mirror":4},"highestEver":214386486937,"upgrades":{"mouse":["doubleClick","biggerTeeth"],"whale":["thinnerWater","biggerBlowhole"],"accountant":["fasterComputers","widerBrains"],"train":["greasyTracks","loudConductors"],"fire":["gasolineFire","cavemanFire"],"boomerang":["spoonerang","doubleRang"],"butterfly":["glassButterfly"],"moon":["lunarPower"],"general":["homage"]},"achievements":{"leaderBoardViewer":true,"seeTheQuade":true,"greenCoin":true}},"UTDLFGZA5":{"coins":246913571,"lastCheck":1646259791.412,"highestEver":246913584,"items":{},"upgrades":{},"achievements":{}},"U0X0ZQCN6":{"coins":100000000,"items":{},"upgrades":{},"achievements":{}},"U02AAB54V34":{"coins":37269811139,"lastCheck":1646320737.911,"highestEver":101412524777,"items":{"mouse":69,"accountant":50,"whale":40,"train":30,"fire":20,"moon":15,"butterfly":5,"boomerang":20},"upgrades":{"mouse":["biggerTeeth","doubleClick"],"accountant":["fasterComputers","widerBrains"],"whale":["biggerBlowhole","thinnerWater"],"train":["greasyTracks","loudConductors"],"fire":["gasolineFire"],"moon":["lunarPower"],"general":["homage"],"boomerang":["spoonerang"]},"achievements":{}},"U02KYLVK1GV":{"coins":946596248,"lastCheck":1646320737.912,"items":{"mouse":69,"accountant":69,"whale":69,"train":69,"fire":52,"boomerang":8,"moon":3,"butterfly":2},"highestEver":37641599401,"upgrades":{"mouse":["doubleClick","biggerTeeth"],"accountant":["fasterComputers","widerBrains"],"whale":["biggerBlowhole","thinnerWater"],"train":["greasyTracks","loudConductors"],"fire":["gasolineFire","cavemanFire"],"boomerang":["spoonerang"],"moon":["lunarPower"],"general":["homage"]},"achievements":{"leaderBoardViewer":true,"seeTheQuade":true}},"U02U15RFK4Y":{"coins":29281767996,"items":{"mouse":40,"accountant":32,"whale":19,"train":13,"fire":13,"boomerang":14,"moon":22,"butterfly":9,"mirror":1},"highestEver":156855843643,"lastCheck":1646320737.911,"upgrades":{"mouse":["doubleClick","biggerTeeth"],"accountant":["fasterComputers","widerBrains"],"whale":["biggerBlowhole"],"train":["greasyTracks"],"fire":["gasolineFire"],"boomerang":["spoonerang"],"moon":["lunarPower"],"butterfly":["glassButterfly"]},"achievements":{"leaderBoardViewer":true,"seeTheQuade":true}},"U2X0SG7BP":{"coins":0,"items":{},"upgrades":{},"achievements":{}},"U017PG4EL1Y":{"coins":4218340103,"items":{"mouse":21,"accountant":23,"whale":22,"fire":8,"train":2,"boomerang":2},"lastCheck":1646320737.912,"highestEver":4218340103,"upgrades":{},"achievements":{}},"U0344TFA7HQ":{"coins":100000101,"items":{},"upgrades":{},"achievements":{}},"U017CB5L1K3":{"coins":100000000,"items":{},"upgrades":{},"achievements":{}},"UQB1E7884":{"coins":100000000,"items":{},"upgrades":{},"achievements":{}}},"nfts":[{"name":"quackers","price":100000000000,"description":"A gorgeously photorealistic picture of a duck.","picture":"``` ______\n/ o \\__ ( Quack! )\n\\ _/\n \\ /\n | \\```","owner":"U02AAB54V34"},{"name":"quackers_imprisoned","price":1000000000000,"description":"The infamous quackers, now in in-jail form","picture":"```| |__|__| | |\n| /| |o |__| (|Quack! )\n| \\| | |_/| |\n| | | | | |\n| || | |\\ | |```","owner":null}]}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "hvacker-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"test": "jest --passWithNoTests",
|
||||||
|
"build": "npm ci",
|
||||||
|
"lint": "standard"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@git.add123.com:7999/add/hvacker-api.git"
|
||||||
|
},
|
||||||
|
"author": "Auto Data Direct, Inc.",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"dependencies": {
|
||||||
|
"@slack/bolt": "^3.9.0",
|
||||||
|
"base-64": "^1.0.0",
|
||||||
|
"express": "^4.17.3",
|
||||||
|
"fs": "0.0.1-security",
|
||||||
|
"jest": "27.0.6"
|
||||||
|
},
|
||||||
|
"standard": {
|
||||||
|
"env": "jest"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
let base64 = require('base-64')
|
||||||
|
let config = require('../config')
|
||||||
|
|
||||||
|
const url = ''
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.append('Authorization', 'Basic ' + base64.encode(config.honeywellKey + ':' + config.honeywellSecret))
|
||||||
|
|
||||||
|
fetch(url, {method:'GET',
|
||||||
|
headers: headers,
|
||||||
|
//credentials: 'user:passwd'
|
||||||
|
}).then(response => response.json())
|
||||||
|
.then(json => console.log(json));
|
|
@ -0,0 +1,21 @@
|
||||||
|
const path = require('path')
|
||||||
|
const homedir = require('os').homedir()
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
const fileName = 'hvackerconfig.json'
|
||||||
|
|
||||||
|
const configPaths = [
|
||||||
|
path.join('/secrets', fileName),
|
||||||
|
path.join(homedir, '.add123', fileName)
|
||||||
|
]
|
||||||
|
|
||||||
|
const getConfigData = () => {
|
||||||
|
const configPath = configPaths.find(fs.existsSync)
|
||||||
|
if (!configPath) {
|
||||||
|
throw 'Could not find a config file!'
|
||||||
|
}
|
||||||
|
const config = fs.readFileSync(configPath, 'utf8')
|
||||||
|
return JSON.parse(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getConfigData()
|
|
@ -0,0 +1,28 @@
|
||||||
|
module.exports = {
|
||||||
|
leaderBoardViewer: {
|
||||||
|
name: 'Leaderboard-Viewer',
|
||||||
|
description: 'Thank you for viewing the leaderboard!',
|
||||||
|
emoji: 'trophy',
|
||||||
|
},
|
||||||
|
seeTheQuade: {
|
||||||
|
name: 'See the Quade',
|
||||||
|
description: 'Quade has appeared in your buyables',
|
||||||
|
emoji: 'quade',
|
||||||
|
},
|
||||||
|
greenCoin: {
|
||||||
|
name: 'Lucky Green Coin',
|
||||||
|
description: 'Its wings smell like onions',
|
||||||
|
emoji: 'money_with_wings'
|
||||||
|
},
|
||||||
|
goldBrick: {
|
||||||
|
name: 'The Golden Brick',
|
||||||
|
description: 'Find a lucky gold brick',
|
||||||
|
emoji: 'goldbrick'
|
||||||
|
},
|
||||||
|
luckyGem: {
|
||||||
|
name: 'Lucky Gem Acquired',
|
||||||
|
description: 'It sparkles',
|
||||||
|
emoji: 'gem'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
module.exports = {
|
||||||
|
mouse: {
|
||||||
|
baseCost: 100,
|
||||||
|
earning: 1,
|
||||||
|
emoji: 'mouse2',
|
||||||
|
description: 'A mouse to steal coins for you.'
|
||||||
|
},
|
||||||
|
accountant: {
|
||||||
|
baseCost: 1_100,
|
||||||
|
earning: 8,
|
||||||
|
emoji: 'male-office-worker',
|
||||||
|
description: 'Legally make money from nothing!'
|
||||||
|
},
|
||||||
|
whale: {
|
||||||
|
baseCost: 12_000,
|
||||||
|
earning: 47,
|
||||||
|
emoji: 'whale',
|
||||||
|
description: 'Someone to spend money on your HVAC Coin mining app.'
|
||||||
|
},
|
||||||
|
train: {
|
||||||
|
baseCost: 130_000,
|
||||||
|
earning: 260,
|
||||||
|
emoji: 'train2',
|
||||||
|
description: 'Efficiently ship your most valuable coins.'
|
||||||
|
},
|
||||||
|
fire: {
|
||||||
|
baseCost: 1_400_000,
|
||||||
|
earning: 1_400,
|
||||||
|
emoji: 'fire',
|
||||||
|
description: 'Return to the roots of HVAC.'
|
||||||
|
},
|
||||||
|
boomerang: {
|
||||||
|
baseCost: 20_000_000,
|
||||||
|
earning: 7_800,
|
||||||
|
emoji: 'boomerang',
|
||||||
|
description: 'Your coin always seems to come back.'
|
||||||
|
},
|
||||||
|
moon: {
|
||||||
|
baseCost: 330_000_000,
|
||||||
|
earning: 44_000,
|
||||||
|
emoji: 'new_moon_with_face',
|
||||||
|
description: 'Convert dark new-moon energy into HVAC Coins.'
|
||||||
|
},
|
||||||
|
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.'
|
||||||
|
},
|
||||||
|
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.'
|
||||||
|
},
|
||||||
|
quade: {
|
||||||
|
baseCost: 1_000_000_000_000,
|
||||||
|
earning: 10_000_000,
|
||||||
|
emoji: 'quade',
|
||||||
|
description: 'Has thumbs capable of physically manipulating the thermostat.'
|
||||||
|
},
|
||||||
|
hvacker: {
|
||||||
|
baseCost: 14_000_000_000_000,
|
||||||
|
earning: 65_000_000,
|
||||||
|
emoji: 'hvacker_angery',
|
||||||
|
description: 'Harness the power of the mad god himself.'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,514 @@
|
||||||
|
const slack = require('../../slack')
|
||||||
|
const fs = require('fs')
|
||||||
|
const jokes = require('./../jokes')
|
||||||
|
const buyableItems = require('./buyableItems')
|
||||||
|
const upgrades = require('./upgrades')
|
||||||
|
const achievements = require('./achievements')
|
||||||
|
|
||||||
|
const saveFile = './hvacoins.json'
|
||||||
|
|
||||||
|
const logError = msg => msg ? console.error(msg) : undefined
|
||||||
|
|
||||||
|
const maybeNews = say => {
|
||||||
|
const random = Math.random()
|
||||||
|
if (random > 0.98) {
|
||||||
|
const prefixedSay = msg => console.log(`Sent news update: '${msg}'`) || say('_Breaking news:_\n' + msg)
|
||||||
|
setTimeout(() => jokes.newsAlert(prefixedSay).catch(logError), 3000)
|
||||||
|
} else if (random > 0.96) {
|
||||||
|
setTimeout(async () => await say('_Say have you heard this one?_'), 3000)
|
||||||
|
setTimeout(() => jokes.tellJoke(say).catch(logError), 4000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commas = num => num.toLocaleString()
|
||||||
|
|
||||||
|
const getItemHeader = user => ([itemName, {baseCost, description, earning, emoji}]) => {
|
||||||
|
const itemCost = commas(user ? calculateCost({itemName, user}) : baseCost)
|
||||||
|
return `*${itemName}* :${emoji}: - ${itemCost} HVAC Coins - ${commas(earning)} CPS\n_${description}_`
|
||||||
|
}
|
||||||
|
const helpText = (highestCoins, user) => Object.entries(buyableItems)
|
||||||
|
.filter(([, value]) => value.baseCost < (highestCoins || 1) * 101)
|
||||||
|
.map(getItemHeader(user))
|
||||||
|
.join('\n\n') +
|
||||||
|
'\n\n:grey_question::grey_question::grey_question:' +
|
||||||
|
'\n\nJust type \'!buy item_name\' to purchase' +
|
||||||
|
'\n\nNote: Listed prices are _base costs_ and will increase as you buy more.'
|
||||||
|
|
||||||
|
const getUpgradeEmoji = upgrade => upgrade.emoji || buyableItems[upgrade.type].emoji
|
||||||
|
const upgradeText = user =>
|
||||||
|
(!user ? '' : '\n\n' + Object.entries(upgrades).filter(([upgradeName, upgrade]) => !hasUpgrade(user, upgrade, upgradeName)).filter(([, upgrade]) => upgrade.condition(user)).map(([key, value]) => `:${getUpgradeEmoji(value)}: *${key}* - ${commas(value.cost)}\n_${value.description}_`).join('\n\n')) +
|
||||||
|
'\n\n:grey_question::grey_question::grey_question:' +
|
||||||
|
'\n\nJust type \'!upgrade upgrade_name\' to purchase'
|
||||||
|
|
||||||
|
const game = JSON.parse(fs.readFileSync(saveFile, 'utf-8'))
|
||||||
|
const { users, nfts } = game
|
||||||
|
|
||||||
|
const saveGame = () => fs.writeFile(saveFile, JSON.stringify(game), logError)
|
||||||
|
|
||||||
|
const getSeconds = () => new Date().getTime() / 1000
|
||||||
|
|
||||||
|
const setHighestCoins = userId => {
|
||||||
|
const prevMax = users[userId].highestEver || 0
|
||||||
|
if (prevMax < users[userId].coins) {
|
||||||
|
users[userId].highestEver = users[userId].coins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUpgrade = (user, upgrade, upgradeName) => user.upgrades[upgrade.type]?.includes(upgradeName)
|
||||||
|
|
||||||
|
const addAchievement = async (user, achievementName, say) => {
|
||||||
|
if (user.achievements[achievementName]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.achievements[achievementName] = true
|
||||||
|
saveGame()
|
||||||
|
await say(`You earned the achievement ${achievements[achievementName].name}!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I'm super not confident this will stay
|
||||||
|
// What's nice is that it centralizes
|
||||||
|
// * calling with event,say,words
|
||||||
|
// * checking for words[1]==='help'
|
||||||
|
const commands = new Map()
|
||||||
|
const command = (commandNames, helpText, action, condition) => {
|
||||||
|
commandNames.forEach(name => commands.set(name, {
|
||||||
|
helpText,
|
||||||
|
action,
|
||||||
|
condition: condition || (() => true)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
command( // I don't like that command() is at the highest indentation level instead of the route name
|
||||||
|
['!t', '!test'],
|
||||||
|
'HelpText: Testing out new routing functionality',
|
||||||
|
async ({ event, say, words }) => {// This extra indendation sucks
|
||||||
|
await say('This is the route acton') // Also the function can't go after command() - not initialized
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
slack.onMessage(async ({ event, say }) => {
|
||||||
|
const words = event?.text?.split(/\s+/) || []
|
||||||
|
const c = commands.get(words[0])
|
||||||
|
if (event.user !== slack.sageUserId) {
|
||||||
|
return // Don't do anything while this is incubating
|
||||||
|
}
|
||||||
|
if (!c?.condition(event.user)) {
|
||||||
|
await say(`Command '${words[0]} not found'`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (words[1] === 'help') {
|
||||||
|
await say(c.helpText)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await c.action({ event, say, words })
|
||||||
|
})
|
||||||
|
// End of new command action
|
||||||
|
|
||||||
|
const listAchievements = async ({ event, say }) => {
|
||||||
|
const user = getUser(event.user)
|
||||||
|
|
||||||
|
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 list = Object.keys(user.achievements)
|
||||||
|
.map(name => achievements[name])
|
||||||
|
.map(({description, emoji, name}) => `:${emoji}: *${name}* - ${description}`)
|
||||||
|
.join('\n')
|
||||||
|
const postfix = achievementCount ? `\n\n_Achievements are boosting your CPS by ${mult.toPrecision(3)}%_` : ''
|
||||||
|
await say(prefix + list + postfix)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getItemCps = (user, itemName) => {
|
||||||
|
const achievements = Object.keys(user.achievements || {}).length
|
||||||
|
const achievementMultiplier = Math.pow(1.01, achievements)
|
||||||
|
const baseCps = (user.items[itemName] || 0) * buyableItems[itemName].earning
|
||||||
|
const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name])
|
||||||
|
return achievementMultiplier * itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), baseCps)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojiLine = (itemName, countOwned) => countOwned < 5
|
||||||
|
? `:${buyableItems[itemName].emoji}:`.repeat(countOwned)
|
||||||
|
: `:${buyableItems[itemName].emoji}: x${countOwned}`
|
||||||
|
const collection = userId =>
|
||||||
|
Object.entries(users[userId]?.items || {})
|
||||||
|
.map(([itemName, countOwned]) => emojiLine(itemName, countOwned) + ' - ' + commas(getItemCps(getUser(userId), itemName)) + ' cps')
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
const getCPS = userId => {
|
||||||
|
const user = getUser(userId)
|
||||||
|
const userItems = user?.items || {}
|
||||||
|
const userGeneralUpgrades = user?.upgrades?.general || []
|
||||||
|
const cpsFromItems = Object.keys(userItems).reduce((total, itemName) => total + getItemCps(getUser(userId), itemName), 0)
|
||||||
|
return Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), cpsFromItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCoins = userId => {
|
||||||
|
const user = users[userId]
|
||||||
|
const currentTime = getSeconds()
|
||||||
|
const lastCheck = user.lastCheck || currentTime
|
||||||
|
const secondsPassed = currentTime - lastCheck
|
||||||
|
user.coins += getCPS(userId) * secondsPassed
|
||||||
|
user.coins = Math.floor(user.coins)
|
||||||
|
|
||||||
|
user.lastCheck = currentTime
|
||||||
|
setHighestCoins(userId)
|
||||||
|
saveGame()
|
||||||
|
return user.coins
|
||||||
|
}
|
||||||
|
|
||||||
|
const mineCoinHelp = 'Mine HVAC coins: `!coin` or `!c`'
|
||||||
|
const mineCoin = async ({ event, say, words }) => {
|
||||||
|
const user = getUser(event.user)
|
||||||
|
if (words[1] === 'help') {
|
||||||
|
await say(mineCoinHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
maybeNews(say)
|
||||||
|
const random = Math.random()
|
||||||
|
const c = getCoins(event.user)
|
||||||
|
const secondsOfCps = seconds => Math.floor(getCPS(event.user) * seconds)
|
||||||
|
let diff
|
||||||
|
let prefix
|
||||||
|
if (random > 0.9967) {
|
||||||
|
diff = 500 + Math.floor(c * 0.10) + secondsOfCps(60 * 30)
|
||||||
|
prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n`
|
||||||
|
await addAchievement(user, 'luckyGem', say)
|
||||||
|
await slack.messageSage(`${slack.ourUsers[event.user]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`)
|
||||||
|
} else if (random > 0.986) {
|
||||||
|
diff = 50 + Math.floor(c * 0.025) + secondsOfCps(60)
|
||||||
|
prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n`
|
||||||
|
await slack.messageSage(`${slack.ourUsers[event.user]} found a lucky gold coin worth ${commas(diff)} HVAC!`)
|
||||||
|
await addAchievement(user, 'goldBrick', say)
|
||||||
|
} else if (random > 0.96) {
|
||||||
|
diff = 10 + Math.floor(c * 0.01) + secondsOfCps(10)
|
||||||
|
prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n`
|
||||||
|
await addAchievement(user, 'greenCoin', say)
|
||||||
|
} else {
|
||||||
|
prefix = `You mined one HVAC.\n`
|
||||||
|
diff = 1
|
||||||
|
}
|
||||||
|
user.coins += diff
|
||||||
|
await say(`${prefix}You now have ${commas(user.coins)} HVAC coin` + (c !== 0 ? 's' : '') + '. Spend wisely.')
|
||||||
|
saveGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
const gambleCoinHelp = 'Gamble away your HVAC: `!gamble` or `!g`\n'+
|
||||||
|
'To use, say `!gamble coin_amount` or `!gamble all`'
|
||||||
|
const gambleCoin = async ({ event, say, words }) => {
|
||||||
|
if (words[1] === 'help') {
|
||||||
|
await say(gambleCoinHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!users[event.user]) {
|
||||||
|
users[event.user] = {
|
||||||
|
coins: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let n
|
||||||
|
let currentCoins = getCoins(event.user)
|
||||||
|
if (words[1].toLowerCase() === 'all') {
|
||||||
|
if (currentCoins === 0) {
|
||||||
|
await say('You don\'t have any coins!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n = currentCoins
|
||||||
|
} else {
|
||||||
|
n = parseInt(event.text.substring(7))
|
||||||
|
}
|
||||||
|
if (!n || n < 0) {
|
||||||
|
await say(`Invalid number '${n}'`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (currentCoins < n) {
|
||||||
|
await say(`You don\'t have that many coins! You have ${commas(currentCoins)}.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
users[event.user].coins -= n
|
||||||
|
let outcome
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
users[event.user].coins += (2 * n)
|
||||||
|
outcome = 'won'
|
||||||
|
} else {
|
||||||
|
outcome = 'lost'
|
||||||
|
}
|
||||||
|
console.log(`They ${outcome}`)
|
||||||
|
await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(users[event.user].coins)}.`)
|
||||||
|
saveGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 buyNftHelp = 'Acquire high-quality art: `!buynft`\n'+
|
||||||
|
'To use, say `!buynft nft_name`'
|
||||||
|
const buyNft = async ({ event, say, words }) => {
|
||||||
|
if (words[1] === 'help') {
|
||||||
|
await say(buyNftHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nft = nfts.find(n => n.name.toLowerCase() === words[1])
|
||||||
|
if (!nft) {
|
||||||
|
const suffix = words[1].match(/[^a-z0-9]/i) ? '. And I\'m unhackable, so cut it out.' : ''
|
||||||
|
await say('No NFT with that name found' + suffix)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (nft.owner) {
|
||||||
|
await say('Someone already owns that NFT!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const c = getCoins(event.user)
|
||||||
|
if (c < nft.price) {
|
||||||
|
await say('You don\'t have enough coin for this nft')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
users[event.user].coins -= nft.price
|
||||||
|
nft.owner = event.user
|
||||||
|
saveGame()
|
||||||
|
await say('You bought ' + nft.name + '!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUser = userId => {
|
||||||
|
if (!users[userId]) {
|
||||||
|
users[userId] = {
|
||||||
|
coins: 0,
|
||||||
|
items: {},
|
||||||
|
upgrades: {},
|
||||||
|
achievements: {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
users[userId].items ??= {}
|
||||||
|
users[userId].upgrades ??= {}
|
||||||
|
users[userId].achievements ??= {}
|
||||||
|
}
|
||||||
|
return users[userId]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buyUpgradeHelp = 'Improve the performance of your HVAC-generators. `!upgrade` or `!u`\n'+
|
||||||
|
'Say `!upgrade` to list available upgrades, or `!upgrade upgrade_name` to purchase.'
|
||||||
|
const buyUpgrade = async ({ userId, say, words }) => {
|
||||||
|
if (words[1] === 'help') {
|
||||||
|
await say(buyUpgradeHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const user = getUser(userId)
|
||||||
|
const upgradeName = words[1]
|
||||||
|
if (!upgradeName) {
|
||||||
|
await say(upgradeText(user))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const upgrade = upgrades[upgradeName]
|
||||||
|
if (!upgrade) {
|
||||||
|
await say('An upgrade with that name does not exist!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!user.upgrades[upgrade.type]) {
|
||||||
|
user.upgrades[upgrade.type] = []
|
||||||
|
}
|
||||||
|
if (hasUpgrade(user, upgrade, upgradeName)) {
|
||||||
|
await say('You already have that upgrade!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!upgrade.condition(user)) {
|
||||||
|
await say('That item does not exist!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const c = getCoins(userId)
|
||||||
|
if (c < upgrade.cost) {
|
||||||
|
await say(`You don't have enough coins! You have ${commas(c)}, but you need ${commas(upgrade.cost)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.coins -= upgrade.cost
|
||||||
|
user.upgrades[upgrade.type].push(upgradeName)
|
||||||
|
await saveGame()
|
||||||
|
await say(`You bought ${upgradeName}!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buyItemHelp = 'Buy new items to earn HVAC with: `!buy` or `!b`\n'+
|
||||||
|
'Use `!buy` to list available items, and `!buy item_name optional_quantity` to get \'em.'
|
||||||
|
const buyItem = async ({ event, say, words }) => {
|
||||||
|
if (words[1] === 'help') {
|
||||||
|
await say(buyItemHelp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const user = getUser(event.user)
|
||||||
|
const buying = words[1]
|
||||||
|
const quantity = parseInt(words[2] || '1')
|
||||||
|
if (!quantity || quantity < 1) {
|
||||||
|
await say('Quantity must be a positive integer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setHighestCoins(event.user)
|
||||||
|
if (!buying) {
|
||||||
|
const highestCoins = user.highestEver || user.coins || 1
|
||||||
|
if (buyableItems.quade.baseCost < highestCoins * 100) {
|
||||||
|
await addAchievement(user, 'seeTheQuade', say)
|
||||||
|
}
|
||||||
|
await say(helpText(highestCoins, user))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const buyable = buyableItems[buying]
|
||||||
|
if (!buyable) {
|
||||||
|
await say('That item does not exist!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const realCost = calculateCost({itemName: buying, user, quantity})
|
||||||
|
const currentCoins = getCoins(event.user)
|
||||||
|
if (currentCoins < realCost) {
|
||||||
|
await say(`You don't have enough coins! You have ${commas(currentCoins)}, but you need ${commas(realCost)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.coins -= realCost
|
||||||
|
user.items[buying] = user.items[buying] || 0
|
||||||
|
user.items[buying] += quantity
|
||||||
|
if (quantity === 1) {
|
||||||
|
await say(`You bought one ${buying}`)
|
||||||
|
} else {
|
||||||
|
await say(`You bought ${quantity} ${buying}`)
|
||||||
|
}
|
||||||
|
saveGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCoinCommand = async ({ target, say }) => {
|
||||||
|
if (!target?.startsWith('<@') || !target.endsWith('>')) {
|
||||||
|
await say('Target must be a valid @')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const targetId = target.substring(2, target.length - 1)
|
||||||
|
if (targetId !== 'U0344TFA7HQ') {
|
||||||
|
const user = getUser(targetId)
|
||||||
|
await say(`<@${targetId}> has ${commas(user.coins)} HVAC.`)
|
||||||
|
} else {
|
||||||
|
const members = (await slack.app.client.conversations.members({channel: slack.temperatureChannelId})).members
|
||||||
|
const humanMembers = members.filter(name => name.length === 11)
|
||||||
|
await say(`Hvacker owns ${humanMembers.length} souls.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gift = async ({ userId, words, say }) => {
|
||||||
|
const user = getUser(userId)
|
||||||
|
const [, target, amountText] = words
|
||||||
|
const amount = amountText === 'all' ? (getCoins(userId)) : parseInt(amountText)
|
||||||
|
if (!amount || amount < 0) {
|
||||||
|
await say('Amount must be a positive integer!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!target?.startsWith('<@') || !target.endsWith('>')) {
|
||||||
|
await say('Target must be a valid @')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (user.coins < amount) {
|
||||||
|
await say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const targetId = target.substring(2, target.length - 1)
|
||||||
|
const targetUser = getUser(targetId)
|
||||||
|
user.coins -= amount
|
||||||
|
targetUser.coins += amount
|
||||||
|
await say(`Gifted ${commas(amount)} HVAC to <@${targetId}>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
slack.onMessage(async ({ event, say }) => {
|
||||||
|
// if (event?.text?.startsWith('!') && event.user !== slack.sageUserId) {
|
||||||
|
// await say('Hvacker is taking a quick nap.')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
const words = event?.text?.split(/\s+/) || []
|
||||||
|
console.log(event?.text, 'by', slack?.ourUsers[event?.user], 'at', new Date().toLocaleTimeString())
|
||||||
|
switch (words[0]) {
|
||||||
|
case '!c':
|
||||||
|
case '!coin':
|
||||||
|
return mineCoin({ event, say, words })
|
||||||
|
case '!g':
|
||||||
|
case '!gamble':
|
||||||
|
return gambleCoin({ event, say, words })
|
||||||
|
case '!b':
|
||||||
|
case '!buy': {
|
||||||
|
return buyItem({event, say, words})
|
||||||
|
}
|
||||||
|
case '!u':
|
||||||
|
case '!upgrade':
|
||||||
|
return buyUpgrade({userId: event.user, say, words})
|
||||||
|
case '!cps':
|
||||||
|
return say(`You are currently earning \`${commas(getCPS(event.user))}\` HVAC Coin per second.`)
|
||||||
|
case '!gift':
|
||||||
|
case '!give':
|
||||||
|
return gift({userId: event.user, words, say})
|
||||||
|
case '!check':
|
||||||
|
return getCoinCommand({target: words[1], say})
|
||||||
|
case '!s':
|
||||||
|
case '!status':
|
||||||
|
return say(
|
||||||
|
`You are currently earning \`${commas(getCPS(event.user))}\` HVAC Coin per second.\n\n` +
|
||||||
|
`You currently have ${commas(getCoins(event.user))} HVAC Coins\n\n` +
|
||||||
|
`${collection(event.user)}\n\n`
|
||||||
|
)
|
||||||
|
case '!a':
|
||||||
|
return listAchievements({ event, say })
|
||||||
|
case '!nfts':
|
||||||
|
const owner = nft => `Owner: *${slack.ourUsers[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 filter = words[1] ? nft => words[1]?.toLowerCase() === nft.name : null
|
||||||
|
|
||||||
|
return say(nfts
|
||||||
|
.filter(filter || (() => true))
|
||||||
|
.map(nftDisplay)
|
||||||
|
.join('\n-------------------------\n') || (filter ? 'No NFTs with that name exist' : 'No NFTs currently exist.')
|
||||||
|
)
|
||||||
|
case '!lb':
|
||||||
|
const user = getUser(event.user)
|
||||||
|
return say('```' +
|
||||||
|
Object.entries(users)
|
||||||
|
.filter(([id]) => getCPS(id) !== 0)
|
||||||
|
.sort(([id], [id2]) => getCPS(id) > getCPS(id2) ? -1 : 1)
|
||||||
|
.map(([id]) => `${slack.ourUsers[id] || '???'} - ${commas(getCPS(id))} CPS - ${commas(getCoins(id))} HVAC`)
|
||||||
|
.join('\n') + '```').then(() => addAchievement(user, 'leaderBoardViewer', say))
|
||||||
|
case '!buynft':
|
||||||
|
return buyNft({event, say, words})
|
||||||
|
case '!ligma':
|
||||||
|
return say(':hvacker_angery:')
|
||||||
|
case '!pog':
|
||||||
|
return say('<https://i.imgur.com/XCg7WDz.png|poggers>')
|
||||||
|
}
|
||||||
|
if (event.user === slack.sageUserId) {
|
||||||
|
const firstWord = words[0]
|
||||||
|
if (firstWord === '!addnft') {
|
||||||
|
const [, 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)
|
||||||
|
const newNft = {
|
||||||
|
name,
|
||||||
|
price: parseInt(price.replace(/,/g, '')),
|
||||||
|
description: desc,
|
||||||
|
picture,
|
||||||
|
owner: null
|
||||||
|
}
|
||||||
|
nfts.push(newNft)
|
||||||
|
console.log('addedNft', newNft)
|
||||||
|
return saveGame()
|
||||||
|
} else if (firstWord === '!s') {
|
||||||
|
const target = words[1]
|
||||||
|
const targetId = target.substring(2, target.length - 1)
|
||||||
|
maybeNews(say)
|
||||||
|
return say(
|
||||||
|
`${target} are currently earning \`${commas(getCPS(targetId))}\` HVAC Coin per second.\n\n` +
|
||||||
|
`They currently have ${commas(getCoins(targetId))} HVAC Coins\n\n` +
|
||||||
|
`${collection(targetId)}\n\n`
|
||||||
|
)
|
||||||
|
} else if (firstWord === '!updatenft') {
|
||||||
|
const nft = nfts.find(n => n.name.toLowerCase() === 'quackers')
|
||||||
|
nft.price *= 100
|
||||||
|
return saveGame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,140 @@
|
||||||
|
const basic = ({ type, description, count, cost }) => ({
|
||||||
|
type,
|
||||||
|
description,
|
||||||
|
condition: user => user.items[type] >= count,
|
||||||
|
cost,
|
||||||
|
effect: itemCps => itemCps * 2
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
doubleClick: basic({
|
||||||
|
type: 'mouse',
|
||||||
|
description: 'Doubles the power of mice',
|
||||||
|
count: 1,
|
||||||
|
cost: 1_000,
|
||||||
|
}),
|
||||||
|
biggerTeeth: basic({
|
||||||
|
type: 'mouse',
|
||||||
|
description: 'Mice can intimidate twice as much HVAC out of their victims.',
|
||||||
|
count: 25,
|
||||||
|
cost: 50_000,
|
||||||
|
}),
|
||||||
|
fasterComputers: basic({
|
||||||
|
type: 'accountant',
|
||||||
|
description: 'Accountants can ~steal~ optimize twice as much HVAC!',
|
||||||
|
count: 1,
|
||||||
|
cost: 11_000,
|
||||||
|
}),
|
||||||
|
widerBrains: basic({
|
||||||
|
type: 'accountant',
|
||||||
|
description: 'For accountant do double of thinking.',
|
||||||
|
count: 25,
|
||||||
|
cost: 550_000,
|
||||||
|
}),
|
||||||
|
biggerBlowhole: basic({
|
||||||
|
type: 'whale',
|
||||||
|
description: 'With all that extra air, whales have double power!',
|
||||||
|
count: 1,
|
||||||
|
cost: 120_000,
|
||||||
|
}),
|
||||||
|
thinnerWater: basic({
|
||||||
|
type: 'whale',
|
||||||
|
description: 'Whales can move twice as quickly through this physics-defying liquid',
|
||||||
|
count: 25,
|
||||||
|
cost: 6_000_000,
|
||||||
|
}),
|
||||||
|
greasyTracks: basic({
|
||||||
|
type: 'train',
|
||||||
|
description: 'Lets trains deliver HVAC twice as efficiently',
|
||||||
|
count: 1,
|
||||||
|
cost: 1_300_000,
|
||||||
|
}),
|
||||||
|
loudConductors: basic({
|
||||||
|
type: 'train',
|
||||||
|
description: 'Conductors can onboard twice as much HVAC',
|
||||||
|
count: 25,
|
||||||
|
cost: 65_000_000,
|
||||||
|
}),
|
||||||
|
gasolineFire: basic({
|
||||||
|
type: 'fire',
|
||||||
|
description: 'Extremely good for breathing in.',
|
||||||
|
count: 1,
|
||||||
|
cost: 14_000_000,
|
||||||
|
}),
|
||||||
|
cavemanFire: basic({
|
||||||
|
type: 'fire',
|
||||||
|
description: 'They just don\'t make \'em like they used to.',
|
||||||
|
count: 25,
|
||||||
|
cost: 700_000_000,
|
||||||
|
}),
|
||||||
|
spoonerang: basic({
|
||||||
|
type: 'boomerang',
|
||||||
|
description: 'Scoops up HVAC mid-flight',
|
||||||
|
count: 1,
|
||||||
|
cost: 200_000_000,
|
||||||
|
}),
|
||||||
|
doubleRang: basic({
|
||||||
|
type: 'boomerang',
|
||||||
|
description: 'You throw one, but somehow catch two',
|
||||||
|
count: 25,
|
||||||
|
cost: 10_000_000_000,
|
||||||
|
}),
|
||||||
|
lunarPower: basic({
|
||||||
|
type: 'moon',
|
||||||
|
description: 'Out with the sol, in with the lun!',
|
||||||
|
count: 1,
|
||||||
|
cost: 3_300_000_000,
|
||||||
|
}),
|
||||||
|
doubleCraters: basic({
|
||||||
|
type: 'moon',
|
||||||
|
description: 'Making every side look like the dark side.',
|
||||||
|
count: 25,
|
||||||
|
cost: 165_000_000_000,
|
||||||
|
}),
|
||||||
|
glassButterfly: basic({
|
||||||
|
type: 'butterfly',
|
||||||
|
description: 'Not your grandma\'s universe manipulation.',
|
||||||
|
count: 1,
|
||||||
|
cost: 51_000_000_000,
|
||||||
|
}),
|
||||||
|
quadWing: basic({
|
||||||
|
type: 'butterfly',
|
||||||
|
description: 'Sounds a lot like a trillion bees buzzing inside your head.',
|
||||||
|
count: 25,
|
||||||
|
cost: 2_550_000_000_000,
|
||||||
|
}),
|
||||||
|
silverMirror: basic({
|
||||||
|
type: 'mirror',
|
||||||
|
description: 'Excellent for stabbing vampires.',
|
||||||
|
count: 1,
|
||||||
|
cost: 750_000_000_000,
|
||||||
|
}),
|
||||||
|
window: basic({
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
fzero: basic({
|
||||||
|
type: 'quade',
|
||||||
|
description: 'Brings out his competitive spirit.',
|
||||||
|
count: 1,
|
||||||
|
cost: 10_000_000_000_000,
|
||||||
|
}),
|
||||||
|
adam: basic({
|
||||||
|
type: 'quade',
|
||||||
|
description: 'He could probably reach the thermostat if he wanted.',
|
||||||
|
count: 25,
|
||||||
|
cost: 500_000_000_000_000,
|
||||||
|
}),
|
||||||
|
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
const routine = require('./routine')
|
||||||
|
|
||||||
|
const emptyBoard = [
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ',]
|
||||||
|
]
|
||||||
|
|
||||||
|
const textFromBoard = board => {
|
||||||
|
const makeRow = r => ` |${r[0]}|${r[1]}|${r[2]}|${r[3]}|${r[4]}|${r[5]}|${r[6]}|`
|
||||||
|
return board.map(makeRow).join('\n') + '\n' +
|
||||||
|
' -----------------\n' +
|
||||||
|
' 1-2-3-4-5-6-7'
|
||||||
|
}
|
||||||
|
|
||||||
|
const numEmojis = ['one', 'two', 'three', 'four', 'five', 'six', 'seven']
|
||||||
|
|
||||||
|
const checkRows = board => {
|
||||||
|
for (const row of board) {
|
||||||
|
const match = row.join('').match(/(XXXX)|(OOOO)/)
|
||||||
|
if (match) {
|
||||||
|
return match[0][0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkColumns = board => {
|
||||||
|
const colToText = i =>
|
||||||
|
board[0][i] +
|
||||||
|
board[1][i] +
|
||||||
|
board[2][i] +
|
||||||
|
board[3][i] +
|
||||||
|
board[4][i] +
|
||||||
|
board[5][i]
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const match = colToText(i).match(/(XXXX)|(OOOO)/)
|
||||||
|
if (match) {
|
||||||
|
return match[0][0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkDiagonals = board => {
|
||||||
|
for (let row = 0; row < 3; row++) {
|
||||||
|
// Starting top-left
|
||||||
|
for (let col = 0; col < 4; col++) {
|
||||||
|
if (board[row][col] !== ' ' &&
|
||||||
|
board[row][col] === board[row + 1][col + 1] &&
|
||||||
|
board[row][col] === board[row + 2][col + 2] &&
|
||||||
|
board[row][col] === board[row + 3][col + 3]
|
||||||
|
){
|
||||||
|
return board[row][col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starting top-right
|
||||||
|
for (let col = 3; col < 7; col++) {
|
||||||
|
if (board[row][col] !== ' ' &&
|
||||||
|
board[row][col] === board[row + 1][col - 1] &&
|
||||||
|
board[row][col] === board[row + 2][col - 2] &&
|
||||||
|
board[row][col] === board[row + 3][col - 3]
|
||||||
|
){
|
||||||
|
return board[row][col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkFull = board => {
|
||||||
|
for (const row of board) {
|
||||||
|
for (const col of row) {
|
||||||
|
if (col !== ' ') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routine.tie
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTurn = board => {
|
||||||
|
let x = 0
|
||||||
|
let o = 0
|
||||||
|
for (const row of board) {
|
||||||
|
for (const col of row) {
|
||||||
|
if (col === 'X') {
|
||||||
|
x += 1
|
||||||
|
} else if (col === 'O') {
|
||||||
|
o += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x > o ? 'O' : 'X'
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeAt = (i, board, char) => {
|
||||||
|
for (let y = 5; y >= 0; y--) {
|
||||||
|
if (board[y][i] === ' ') {
|
||||||
|
board[y][i] = char
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMove = (emoji, board) =>
|
||||||
|
placeAt(numEmojis.indexOf(emoji), board, getTurn(board))
|
||||||
|
|
||||||
|
routine.build({
|
||||||
|
startTriggers: ['connect 4', 'c4'],
|
||||||
|
initialBoard: () => emptyBoard,
|
||||||
|
turnChoiceEmojis: numEmojis,
|
||||||
|
gameName: 'Connect 4',
|
||||||
|
textFromBoard,
|
||||||
|
checkWinner: board => checkRows(board) || checkColumns(board) || checkDiagonals(board) || checkFull(board),
|
||||||
|
makeMove
|
||||||
|
})
|
|
@ -0,0 +1,4 @@
|
||||||
|
require('./connect4')
|
||||||
|
require('./tictactoe')
|
||||||
|
require('./jokes')
|
||||||
|
require('./coins')
|
|
@ -0,0 +1,108 @@
|
||||||
|
const slack = require('../slack')
|
||||||
|
|
||||||
|
// TODO: Move jokes/news into their own files, and let hvacker edit them when !addjoke or !addnews are used
|
||||||
|
const jokes = [
|
||||||
|
['What do you call a duck that steals things from the bathroom?', 'A robber ducky.'],
|
||||||
|
['On what side does a duck have the most feathers?', 'The outside.'],
|
||||||
|
['Why did the duck cross the playground?', 'To get to the other slide.'],
|
||||||
|
['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'],
|
||||||
|
['Why do ducks lay eggs?', 'They would break if they dropped them.'],
|
||||||
|
['Why do ducks quack?', 'Well, because they can\'t oink, or moo, or bark.'],
|
||||||
|
['Why do ducks fly south for the winter?', 'It\'s too far to waddle.'],
|
||||||
|
['Why did the duck cross the road?', 'To show the chicken how to do it.'],
|
||||||
|
// Puns:
|
||||||
|
['Why do ducks make good detectives?', 'Because they always quack the case!'],
|
||||||
|
['When does a duck get up in the morning?', 'At the quack of dawn!'],
|
||||||
|
['What do you call a duck that loves fireworks?', 'A fire-quacker.'],
|
||||||
|
['What did the duck say to the waiter?', '"Put it on my bill."'],
|
||||||
|
['Where do sick ducks go?', 'To the Ductor!'],
|
||||||
|
['What kind of TV shows do ducks watch?', 'Duckumenteries!'],
|
||||||
|
['What type of food do you get when you cross a duck with a mole?', 'Duckamole!'],
|
||||||
|
['What did the duck say when he dropped the dishes?', '"I hope I didn\'t quack any!"'],
|
||||||
|
['What is a duck\'s favourite game?', '"Beak-a-boo!"'],
|
||||||
|
['Why did the duck cross the road?', 'Because there was a quack in the pavement!'],
|
||||||
|
['What has webbed feet and fangs?', 'Count Duckula!'],
|
||||||
|
['What do ducks get after they eat?', 'A bill.'],
|
||||||
|
['What do ducks eat with their soup?', 'Quackers.'],
|
||||||
|
['What happens when you say something funny to a duck?', 'It quacks up.'],
|
||||||
|
['What\'s a duck\'s favourite ballet?', 'The Nutquacker.'],
|
||||||
|
['What do ducks say when people throw things at them?', '"Time to duck!"'],
|
||||||
|
['Why are ducks so good at fixing things?', 'Because they\'re great at using duck-tape!'],
|
||||||
|
['What do you get when you put a bunch of rubber ducks in a box?', 'A box of quackers.'],
|
||||||
|
['Why was the teacher annoyed with the duck?', 'Because it wouldn\'t stop quacking jokes!'],
|
||||||
|
['What did the duck eat for a snack?', 'Salted quackers!'],
|
||||||
|
['What do you call a rude duck?', 'A duck with a quackitude.'],
|
||||||
|
['What did the lawyer say to the duck in court?', '"I demand an egg-splanation!"'],
|
||||||
|
['How can you tell rubber ducks apart?', 'You can\'t, they look egg-xactly the same!'],
|
||||||
|
['Why are ducks good at budgeting?', 'They know how to handle the bills!'],
|
||||||
|
['Where do tough ducks come from?', 'Hard-boiled eggs.'],
|
||||||
|
['Why do ducks have webbed feet?', 'To stomp out fires.', 'Why do elephants have big feet?', 'To stomp out burning ducks.'],
|
||||||
|
['Why do ducks check the news?', 'For the feather forecast.'],
|
||||||
|
['What did the ducks carry their schoolbooks in?', 'Their quack-packs.'],
|
||||||
|
['What do you call it when it\'s raining ducks and chickens?', 'Fowl weather.'],
|
||||||
|
['Why did the duck get a red card in the football game?', 'For Fowl-play.'],
|
||||||
|
['What did the duck say to the spider?', '"Why don\'t you have webbed feet?"'],
|
||||||
|
['What do you get if you cross a duck with an accountant?', 'A bill with a bill.'],
|
||||||
|
['What do you call a duck\'s burp?', 'A fowl smell!'],
|
||||||
|
['What do you get if you cross a vampire, duck and a sheep?', 'Count Duck-ewe-la.'],
|
||||||
|
['What do you call a duck that\'s very clever?', 'A wise-quacker.'],
|
||||||
|
['Why do ducks never ask for directions?', 'They prefer to wing it.'],
|
||||||
|
// I wrote dis
|
||||||
|
['What did the man say to his wife when a duck flew at her head?', '"Look out! There\'s a duck flying at your head!"'],
|
||||||
|
['What kind of duck plays goalie?', 'A hockey duck.'],
|
||||||
|
['What kind of bird gets a job here?', 'A software duckveloper!'],
|
||||||
|
['How many ducks does it take to screw in a light bulb?', 'Ducks do not live indoors.'],
|
||||||
|
['What kind of drug does a duck like?', 'Quack.']
|
||||||
|
]
|
||||||
|
|
||||||
|
const news = [
|
||||||
|
'Duck criminal escapes from duck prison. Whereabouts unknown.',
|
||||||
|
'Criminal mastermind duck believed to have taken refuge on Slack.',
|
||||||
|
'Infamous _"quackers"_ NFT may be related to recent Chicago crime spree. More at 11.',
|
||||||
|
'Six geese arrested under suspicion of honking.',
|
||||||
|
'Swan under investigation for illegal trumpet-smuggling ring.',
|
||||||
|
'Local rooster the subject of serious egg-stealing allegations.',
|
||||||
|
'Inducknesian court rules that the news is certainly _not_ controlled by an all-powerful duck lord, and that everyone should please stop asking about it.',
|
||||||
|
'"Spending 6,000,000,000 HVAC on a space yacht was the best decision of my life", reveals local billionaire/jerk.',
|
||||||
|
'16 Unethical Egg Hacks You Won\'t Learn In School. You can\'t believe number 5!',
|
||||||
|
'The word "Tuesday" declared illegal. Refer to it as "Quackday" from now on.',
|
||||||
|
'Has anyone been killed/maimed/publicly humiliated for not following the duck lord\'s orders? The answer might surprise you.',
|
||||||
|
'_They called him crazy -_ Local duck bores to the center of the earth and lives with the mole people.',
|
||||||
|
'Jerry Seinfeld - Love affair with a hen! Is this the new Hollywood power couple?',
|
||||||
|
'Is your uncle secretly a goose? There\'s simply no way of knowing.',
|
||||||
|
'Danny Duckvito considers opening his own chain of supermarkets. He is quoted as saying "Heyyyy, I\'m Danny Duckvito"',
|
||||||
|
'Slack-related-gambling epidemic! People around the globe are betting their digital lives away.'
|
||||||
|
]
|
||||||
|
|
||||||
|
const tellJoke = async say => {
|
||||||
|
const joke = jokes[Math.floor(Math.random() * jokes.length)]
|
||||||
|
|
||||||
|
let timeout = 0
|
||||||
|
for (const part of joke) {
|
||||||
|
setTimeout(async () => await say(part), timeout);
|
||||||
|
timeout += 5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newsAlert = async say => {
|
||||||
|
let article = lastNews
|
||||||
|
while(article === lastNews) {
|
||||||
|
article = Math.floor(Math.random() * news.length)
|
||||||
|
}
|
||||||
|
lastNews = article
|
||||||
|
await say(news[article])
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastNews = -1
|
||||||
|
slack.onMessage(async ({ event, say }) => {
|
||||||
|
if (event.text?.toLowerCase() === '!joke') {
|
||||||
|
await tellJoke(say)
|
||||||
|
} else if (event.text?.toLowerCase() === '!news') {
|
||||||
|
await newsAlert(say)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tellJoke,
|
||||||
|
newsAlert,
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
const slack = require('../slack')
|
||||||
|
|
||||||
|
const tie = 'TIE'
|
||||||
|
|
||||||
|
const messageFromBoard = ({ dataName, gameName, textFromBoard, board, player1, player2 }) =>
|
||||||
|
gameName + ' between ' + player1.toUpperCase() + ' and ' + player2.toUpperCase() + ' ' + encodeGame(dataName, board, [player1, player2]) + '\n' +
|
||||||
|
'```' + textFromBoard(board) + '\n```'
|
||||||
|
|
||||||
|
const addChoiceEmojis = async ({ choices, channel, ts }) => {
|
||||||
|
const addEmoji = async emojiName =>
|
||||||
|
await slack.app.client.reactions.add({
|
||||||
|
channel,
|
||||||
|
timestamp: ts,
|
||||||
|
name: emojiName
|
||||||
|
})
|
||||||
|
for (const choice of choices) {
|
||||||
|
await addEmoji(choice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildGameStarter = ({ startTriggers, dataName, gameName, textFromBoard, initialBoard, turnChoiceEmojis }) => async ({ event, say }) => {
|
||||||
|
if (event.channel_type === 'im') {
|
||||||
|
const eventText = event.text?.toLowerCase()
|
||||||
|
if (eventText && startTriggers.find(keyword => eventText.startsWith('!' + keyword))) {
|
||||||
|
const opponent = event.text.toUpperCase().match(/<@[^>]*>/)[0]
|
||||||
|
const msg = messageFromBoard({
|
||||||
|
dataName,
|
||||||
|
gameName,
|
||||||
|
textFromBoard,
|
||||||
|
board: initialBoard(),
|
||||||
|
player1: '<@' + event.user + '>',
|
||||||
|
player2: opponent
|
||||||
|
})
|
||||||
|
const sent = await say(msg)
|
||||||
|
await addChoiceEmojis({...sent, choices: turnChoiceEmojis})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodeGame = (dataKey, board, players) => slack.encodeData(dataKey, {board, players})
|
||||||
|
|
||||||
|
const decodeGame = (dataKey, message) => slack.decodeData(dataKey, message)
|
||||||
|
|
||||||
|
const getMessages = winner => {
|
||||||
|
const buildMessage = state => !winner ? '' : winner === tie ? '\nIt\'s a tie!' : `\nYou ${state}!`
|
||||||
|
return {
|
||||||
|
you: buildMessage('win'),
|
||||||
|
opponent: buildMessage('lost')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildTurnHandler = ({ gameName, dataName, checkWinner, textFromBoard, turnChoiceEmojis, makeMove }) => async ({ event, say }) => {
|
||||||
|
if (event.item_user !== slack.hvackerBotUserId || !turnChoiceEmojis.includes(event.reaction)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await slack.getMessage(event.item)
|
||||||
|
if (!message.messages[0]?.text?.includes(gameName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = decodeGame(dataName, message.messages[0].text)
|
||||||
|
if (!game) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { board, players } = game
|
||||||
|
let winner = checkWinner(board)
|
||||||
|
if (winner) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [player1, player2] = players
|
||||||
|
let opponent = (player1.includes(event.user) ? player2 : player1)
|
||||||
|
opponent = opponent.replace(/[^A-Z0-9]/gi, '').toUpperCase()
|
||||||
|
|
||||||
|
if (!makeMove(event.reaction, board)) {
|
||||||
|
await say('You can\'t go there!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
winner = checkWinner(board)
|
||||||
|
|
||||||
|
const boardMessage = messageFromBoard({
|
||||||
|
dataName,
|
||||||
|
gameName,
|
||||||
|
textFromBoard,
|
||||||
|
board,
|
||||||
|
player1,
|
||||||
|
player2
|
||||||
|
})
|
||||||
|
const winnerMessages = getMessages(winner)
|
||||||
|
await say(boardMessage + winnerMessages.you)
|
||||||
|
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)
|
||||||
|
const sentBoard = await slack.app.client.chat.postMessage({
|
||||||
|
channel: opponent,
|
||||||
|
text: boardMessage + winnerMessages.opponent
|
||||||
|
})
|
||||||
|
if (!winner) {
|
||||||
|
await addChoiceEmojis({...sentBoard, choices: turnChoiceEmojis})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tie,
|
||||||
|
build: ({
|
||||||
|
startTriggers,
|
||||||
|
initialBoard,
|
||||||
|
turnChoiceEmojis,
|
||||||
|
gameName,
|
||||||
|
textFromBoard,
|
||||||
|
checkWinner,
|
||||||
|
makeMove,
|
||||||
|
}) => {
|
||||||
|
const dataName = gameName.replace(/[^0-9a-zA-Z]/gi, '')
|
||||||
|
const gameStarter = buildGameStarter({
|
||||||
|
startTriggers,
|
||||||
|
gameName,
|
||||||
|
dataName,
|
||||||
|
initialBoard,
|
||||||
|
textFromBoard,
|
||||||
|
turnChoiceEmojis
|
||||||
|
});
|
||||||
|
slack.onMessage(gameStarter)
|
||||||
|
|
||||||
|
const turnHandler = buildTurnHandler({
|
||||||
|
gameName,
|
||||||
|
dataName,
|
||||||
|
checkWinner,
|
||||||
|
textFromBoard,
|
||||||
|
turnChoiceEmojis,
|
||||||
|
makeMove
|
||||||
|
})
|
||||||
|
slack.onReaction(turnHandler)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
const routine = require("./routine");
|
||||||
|
|
||||||
|
const emptyBoard = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
|
||||||
|
|
||||||
|
const textFromBoard = board =>
|
||||||
|
` ${board[0]} | ${board[1]} | ${board[2]} \n` +
|
||||||
|
`-----------\n` +
|
||||||
|
` ${board[3]} | ${board[4]} | ${board[5]} \n` +
|
||||||
|
`-----------\n` +
|
||||||
|
` ${board[6]} | ${board[7]} | ${board[8]}`
|
||||||
|
|
||||||
|
const numEmojis = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
|
||||||
|
|
||||||
|
const winningThrees = [
|
||||||
|
[0, 1, 2],
|
||||||
|
[3, 4, 5],
|
||||||
|
[6, 7, 8],
|
||||||
|
|
||||||
|
[0, 3, 6],
|
||||||
|
[1, 4, 7],
|
||||||
|
[2, 5, 8],
|
||||||
|
|
||||||
|
[0, 4, 8],
|
||||||
|
[2, 4, 6],
|
||||||
|
]
|
||||||
|
|
||||||
|
const checkWinner = board => {
|
||||||
|
for (const [a, b, c] of winningThrees) {
|
||||||
|
if (board[a] !== ' ' && board[a] === board[b] && board[a] === board[c]) {
|
||||||
|
return board[a]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
if (board[i] === ' ') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routine.tie
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTurn = board => {
|
||||||
|
let x = 0
|
||||||
|
let o = 0
|
||||||
|
board.forEach(spot => {
|
||||||
|
if (spot === 'X') {
|
||||||
|
x += 1
|
||||||
|
} else if (spot === 'O') {
|
||||||
|
o += 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return x > o ? 'O' : 'X'
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeAt = (i, board, char) => {
|
||||||
|
if (board[i] === ' ') {
|
||||||
|
board[i] = char
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyTurn = (emoji, board) =>
|
||||||
|
placeAt(numEmojis.indexOf(emoji), board, getTurn(board))
|
||||||
|
|
||||||
|
routine.build({
|
||||||
|
startTriggers: ['ttt', 'tictactoe', 'tic-tac-toe'],
|
||||||
|
initialBoard: () => emptyBoard,
|
||||||
|
turnChoiceEmojis: ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'],
|
||||||
|
gameName: 'Tic Tac Toe',
|
||||||
|
textFromBoard,
|
||||||
|
makeMove: applyTurn,
|
||||||
|
checkWinner
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
const postToHoneywell = options => {}
|
||||||
|
|
||||||
|
const postTemp = ({ heatSetpoint, coolSetpoint, mode}) => {
|
||||||
|
postToHoneywell({
|
||||||
|
heatSetpoint,
|
||||||
|
coolSetpoint,
|
||||||
|
mode
|
||||||
|
/* thermostatSetpointStatus or autoChangeoverActive depending on TCC or LCC device.
|
||||||
|
* or maybe nothing, the docs aren't crazy clear */
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentTemp = () => 76
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
postTemp,
|
||||||
|
getCurrentTemp
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
const { postTemp, getCurrentTemp } = require('./honeywell')
|
||||||
|
const { onTempChangeRequested } = require('./slack')
|
||||||
|
require('./games')
|
||||||
|
|
||||||
|
const coolMode = 'Cool'
|
||||||
|
const heatMode = 'Heat'
|
||||||
|
|
||||||
|
const minTemp = 68
|
||||||
|
const maxTemp = 80
|
||||||
|
|
||||||
|
let lowTemp = 72
|
||||||
|
let highTemp = 74
|
||||||
|
|
||||||
|
const cleanTemp = temp => {
|
||||||
|
if (temp > maxTemp) {
|
||||||
|
temp = maxTemp
|
||||||
|
} else if (temp < minTemp) {
|
||||||
|
temp = minTemp
|
||||||
|
}
|
||||||
|
return temp
|
||||||
|
}
|
||||||
|
|
||||||
|
onTempChangeRequested(change => {
|
||||||
|
const { indoorTemperature } = getCurrentTemp()
|
||||||
|
switch (change) {
|
||||||
|
case 'Hotter': {
|
||||||
|
lowTemp += 2
|
||||||
|
highTemp += 2
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Colder': {
|
||||||
|
lowTemp -= 2
|
||||||
|
highTemp -= 2
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Good': {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
highTemp = cleanTemp(highTemp)
|
||||||
|
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
|
||||||
|
|
||||||
|
if (!mode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
postTemp({
|
||||||
|
coolSetpoint: lowTemp,
|
||||||
|
heatSetpoint: highTemp,
|
||||||
|
mode
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,235 @@
|
||||||
|
const { App: SlackApp } = require('@slack/bolt')
|
||||||
|
const config = require('../config')
|
||||||
|
|
||||||
|
const temperatureChannelId = 'C034156CE03'
|
||||||
|
const hvackerBotUserId = 'U0344TFA7HQ'
|
||||||
|
const sageUserId = 'U028BMEBWBV'
|
||||||
|
|
||||||
|
const pollingMinutes = 10
|
||||||
|
const pollingPeriod = 1000 * 60 * pollingMinutes
|
||||||
|
|
||||||
|
const colderEmoji = 'snowflake'
|
||||||
|
const hotterEmoji = 'fire'
|
||||||
|
const goodEmoji = '+1'
|
||||||
|
|
||||||
|
let app
|
||||||
|
try {
|
||||||
|
app = new SlackApp({
|
||||||
|
token: config.slackBotToken,
|
||||||
|
signingSecret: config.slackSigningSecret,
|
||||||
|
appToken: config.slackAppToken,
|
||||||
|
socketMode: true
|
||||||
|
})
|
||||||
|
// app.client.conversations.list({types: 'private_channel'}).then(fetched => {
|
||||||
|
// 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 halfTriggers = ['change temperature', "i'm cold", "i'm hot", 'quack', 'hvacker', '<@U0344TFA7HQ>']
|
||||||
|
|
||||||
|
const sendHelp = async (say, prefix) => {
|
||||||
|
if (prefix) {
|
||||||
|
prefix = prefix + '\n'
|
||||||
|
} else {
|
||||||
|
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.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMessage = async ({channel, ts}) => app.client.conversations.history({
|
||||||
|
channel: channel,
|
||||||
|
latest: ts,
|
||||||
|
inclusive: true,
|
||||||
|
limit: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
app.event('reaction_added', async ({ event, context, client, say }) => {
|
||||||
|
for (const listener of reactionListeners) {
|
||||||
|
listener({ event, say })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ourUsers = {
|
||||||
|
'U028BMEBWBV': 'Sage',
|
||||||
|
'U02U15RFK4Y': 'Adam',
|
||||||
|
'U02AAB54V34': 'Houston',
|
||||||
|
'U02KYLVK1GV': 'Quade',
|
||||||
|
'U017PG4EL1Y': 'Max',
|
||||||
|
'UTDLFGZA5': 'Tyler'
|
||||||
|
}
|
||||||
|
|
||||||
|
let activePolls = {}
|
||||||
|
app.event('message', async ({ event, context, client, say }) => {
|
||||||
|
console.log(event)
|
||||||
|
for (const listener of messageListeners) {
|
||||||
|
listener({ event, say })
|
||||||
|
}
|
||||||
|
const words = event.text?.split('\s')
|
||||||
|
if (event.user === 'U028BMEBWBV' && event.channel === 'D0347Q4H9FE') {
|
||||||
|
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())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let eventText = event.text?.toLowerCase() || ''
|
||||||
|
|
||||||
|
if (eventText.startsWith('!help')) {
|
||||||
|
await sendHelp(say)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pollTriggers.includes(eventText)) {
|
||||||
|
if (halfTriggers.includes(eventText)) {
|
||||||
|
await sendHelp(say, 'It looks like you might want to change the temperature.')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePolls[event.channel]) {
|
||||||
|
await postToTechThermostatChannel({ text: "There's already an active poll in this channel!" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activePolls[event.channel] = true
|
||||||
|
const pollTs = await startPoll()
|
||||||
|
setTimeout(async () => {
|
||||||
|
const reactions = await app.client.reactions.get({
|
||||||
|
channel: temperatureChannelId,
|
||||||
|
timestamp: pollTs,
|
||||||
|
full: true
|
||||||
|
})
|
||||||
|
const reactCounts = {}
|
||||||
|
reactions.message.reactions.forEach(reaction => reactCounts[reaction.name] = reaction.count)
|
||||||
|
|
||||||
|
const contentVotes = reactCounts[goodEmoji]
|
||||||
|
const hotterVotes = reactCounts[hotterEmoji]
|
||||||
|
const colderVotes = reactCounts[colderEmoji]
|
||||||
|
|
||||||
|
let text = 'The people have spoken, and would like to '
|
||||||
|
if (hotterVotes > colderVotes && hotterVotes > contentVotes) {
|
||||||
|
text += 'raise the temperature, quack.'
|
||||||
|
requestTempChange('Hotter')
|
||||||
|
} else if (colderVotes > hotterVotes && colderVotes > contentVotes) {
|
||||||
|
text += 'lower the temperature, quack quack.'
|
||||||
|
requestTempChange('Colder')
|
||||||
|
} else {
|
||||||
|
text += 'keep the temperature as-is, quaaack.'
|
||||||
|
requestTempChange('Good')
|
||||||
|
}
|
||||||
|
|
||||||
|
await postToTechThermostatChannel({ text })
|
||||||
|
delete activePolls[event.channel]
|
||||||
|
}, pollingPeriod)
|
||||||
|
})
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
await app.start().catch(console.error)
|
||||||
|
console.log('Slack Bolt has started')
|
||||||
|
//setTimeout(async () => {
|
||||||
|
// await messageSage('<https://i.imgur.com/VCvfvdz.png|...>')
|
||||||
|
//}, 2000)
|
||||||
|
})();
|
||||||
|
|
||||||
|
const postToTechThermostatChannel = async optionsOrText => {
|
||||||
|
if (optionsOrText === null || typeof optionsOrText !== 'object') {
|
||||||
|
optionsOrText = {
|
||||||
|
text: optionsOrText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await app.client.chat.postMessage({...optionsOrText, channel: temperatureChannelId})
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageSage = async optionsOrText => messageIn(sageUserId, optionsOrText)
|
||||||
|
const messageQuade = async optionsOrText => messageIn('U02KYLVK1GV', optionsOrText)
|
||||||
|
|
||||||
|
const messageIn = async (channel, optionsOrText) => {
|
||||||
|
if (optionsOrText === null || typeof optionsOrText !== 'object') {
|
||||||
|
optionsOrText = {
|
||||||
|
text: optionsOrText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await app.client.chat.postMessage({...optionsOrText, channel})
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPoll = async () => {
|
||||||
|
const sent = await postToTechThermostatChannel({
|
||||||
|
text: `<!here|here> Temperature poll requested! In ${pollingMinutes} minute(s) 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!)`
|
||||||
|
})
|
||||||
|
const addReaction = async emojiName =>
|
||||||
|
await app.client.reactions.add({
|
||||||
|
channel: temperatureChannelId,
|
||||||
|
timestamp: sent.ts,
|
||||||
|
name: emojiName
|
||||||
|
})
|
||||||
|
await addReaction(colderEmoji)
|
||||||
|
await addReaction(hotterEmoji)
|
||||||
|
await addReaction(goodEmoji)
|
||||||
|
return sent.ts
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempChangeListeners = []
|
||||||
|
const messageListeners = []
|
||||||
|
const reactionListeners = []
|
||||||
|
|
||||||
|
const requestTempChange = change => {
|
||||||
|
tempChangeListeners.forEach(listener => listener(change))
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodeData = (key, data) =>
|
||||||
|
`<http://${key}ZZZ${Buffer.from(JSON.stringify(data), 'utf-8').toString('base64')}| >`
|
||||||
|
|
||||||
|
const decodeData = (key, message) => {
|
||||||
|
const regex = new RegExp(`http://${key}ZZZ[^|]*`)
|
||||||
|
let match = message.match(regex)
|
||||||
|
if (!match) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
match = match[0].substring(10 + key.length) // 10 === 'http://'.length + 'ZZZ'.length
|
||||||
|
return JSON.parse(Buffer.from(match, 'base64').toString('utf-8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReaction = listener => reactionListeners.push(listener)
|
||||||
|
|
||||||
|
onReaction(async ({ event, say }) => {
|
||||||
|
if (event.user === sageUserId && event.reaction === 'x') {
|
||||||
|
console.log(event)
|
||||||
|
await app.client.chat.delete({ channel: event.item.channel, ts: event.item.ts })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
app,
|
||||||
|
hvackerBotUserId,
|
||||||
|
temperatureChannelId,
|
||||||
|
onAction: app.action,
|
||||||
|
getMessage,
|
||||||
|
updateMessage: app.client.chat.update,
|
||||||
|
postToTechThermostatChannel,
|
||||||
|
onTempChangeRequested: listener => tempChangeListeners.push(listener),
|
||||||
|
onMessage: listener => messageListeners.push(listener),
|
||||||
|
onReaction,
|
||||||
|
encodeData,
|
||||||
|
decodeData,
|
||||||
|
sageUserId,
|
||||||
|
messageSage,
|
||||||
|
ourUsers
|
||||||
|
}
|
Loading…
Reference in New Issue