const { App } = require('@slack/bolt'); const postgres = require('postgres'); require('dotenv').config() const sql = postgres({ host: '/var/run/postgresql', database: 'haroon_slackmaster', username: 'haroon' }) const SpecialOpponents = require('./opponents/special'); const BeginnerOpponents = require('./opponents/beginner'); const AllOpponents = [ ...(SpecialOpponents.map(x => ({ ...x, rank: 'SPECIAL' }))), ...(BeginnerOpponents.map(x => ({ ...x, rank: 'BEGINNER' }))), ] const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET }); async function initializeUser(slackUserId) { let a = await sql`SELECT * FROM users WHERE slack_id = ${slackUserId};` if (a.length === 0) { a = await sql`INSERT INTO users (slack_id) VALUES (${slackUserId}) RETURNING *;` await sql`INSERT INTO cooldowns (slack_id) VALUES (${slackUserId});` await sql`INSERT INTO customization (slack_id) VALUES (${slackUserId});` } return a[0]; } async function getBattlerUrl(slackUserId, endpoint = "battler.png") { const [ user ] = await sql`SELECT * FROM customization WHERE slack_id = ${slackUserId};` const url = new URL(endpoint, "https://generator.battlemaster.obl.ong/"); const a = Object.entries(user); for (let x of a) { if (x[0] == "slack_id") continue; url.searchParams.append(x[0], x[1] || "") } return url.href; } app.use(async (ctx) => { await initializeUser(ctx.context.userId) await ctx.next() }) app.command('/chooseopponent', async (ctx) => { await ctx.ack(); const user = await initializeUser(ctx.body.user_id); if (user.currentopponent != "None") { const opponent = AllOpponents.find(x => x.rawId == user.currentopponent) return await ctx.respond({ response_type: 'ephemeral', text: `You are already in a battle with ${opponent.name}.`, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* You are already in a battle with *${opponent.name}*. Please finish your battle with them before proceeding. ${user.battlemessage}` } } ] }) } await ctx.client.views.open({ trigger_id: ctx.body.trigger_id, view: { "private_metadata": ctx.payload.channel_id, "type": "modal", "callback_id": "chooseopponent", "title": { "type": "plain_text", "text": "Choose an opponent", "emoji": true }, "submit": { "type": "plain_text", "text": "Choose", "emoji": true }, "close": { "type": "plain_text", "text": "Never mind", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `*Battle Support*: Hiya <@${ctx.body.user_id}>! What rank opponent would you like to battle against?\n\nNot sure yet? Don't worry! Just cancel out and view what opponents you can fight with \`/viewopponents\`!` } }, { "type": "divider" }, { "type": "input", "element": { "type": "radio_buttons", "options": [ { "text": { "type": "plain_text", "text": "Special", "emoji": true }, "value": "SPECIAL" }, { "text": { "type": "plain_text", "text": "Beginner", "emoji": true }, "value": "BEGINNER" }, { "text": { "type": "plain_text", "text": "Casual", "emoji": true }, "value": "CASUAL" } ], "action_id": "rank-selection" }, "label": { "type": "plain_text", "text": "Choose a rank:", "emoji": true } } ] } }) }); async function generateProfile(dbUser, slackUser) { return [ { "type": "section", "fields": [ { "type": "mrkdwn", "text": "*User:* " + slackUser.display_name_normalized }, { "type": "mrkdwn", "text": "*Rank:* " + dbUser.rank }, { "type": "mrkdwn", "text": "*Battle Power*: " + (dbUser.health + dbUser.mindmg + dbUser.maxdmg) }, { "type": "mrkdwn", "text": `*Victories/Losses:* ${dbUser.victories}/${dbUser.losses} (${(dbUser.victories / (dbUser.victories + dbUser.losses)) * 100 || 0}%)` }, { "type": "mrkdwn", "text": "*Current Win Streak:* " + dbUser.curstreak }, { "type": "mrkdwn", "text": "*Highest Win Streak*: " + dbUser.highstreak } ], "accessory": { "type": "image", "image_url": slackUser.image_1024, "alt_text": "user profile" } }, { "type": "section", "fields": [ { "type": "mrkdwn", "text": `*Base Health:* ${dbUser.health}\n*Base Min Damage:* ${dbUser.mindmg}\n*Base Max Damage:* ${dbUser.maxdmg}` }, { "type": "mrkdwn", "text": `*Creation Shards:* ${dbUser.cshards}\n*Destruction Shards:* ${dbUser.dshards}\n*Skill Points:* ${dbUser.spoints}` } ], "accessory": { type: "image", image_url: (await getBattlerUrl(dbUser.slack_id, "profilebattler.png")) + "&username=" + slackUser.display_name_normalized, alt_text: "user battler" } } ] } app.command('/profile', async (ctx) => { await ctx.ack(); const args = ctx.body.text.slice().split(/ +/g).filter(x => x); let match; // If there is an argument and the first one is a Slack ping if (args.length && (match = args[0].match(/\<\@(.+)\|(.+)>/))) { const mentionedUser = match[1]; const dbUser = await initializeUser(mentionedUser); const slackUser = (await ctx.client.users.info({ user: mentionedUser })).user.profile; ctx.say({ "text": `@${slackUser.display_name_normalized}'s profile`, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `<@${ctx.body.user_id}> ran \`/profile @${slackUser.display_name_normalized}\`` } }, ...await generateProfile(dbUser, slackUser) ] }) } // If there is an argument but it isn't a Slack ping else if (args.length) { ctx.respond({ blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Greetings <@${ctx.body.user_id}>. You have tried to view the profile of an invalid user. Please ensure you either send a user ping as an argument or provide no argument at all.` } } ] }) } // There is no argument else { const dbUser = await initializeUser(ctx.body.user_id); const slackUser = (await ctx.client.users.info({ user: ctx.body.user_id })).user.profile; ctx.say({ "text": `@${slackUser.display_name_normalized}'s profile`, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `<@${ctx.body.user_id}> ran \`/profile\`` } }, ...await generateProfile(dbUser, slackUser) ] }) } }) app.view("chooseopponent", async (ctx) => { const user = await initializeUser(ctx.context.userId); const { selected_option } = Object.values(ctx.view.state.values)[0]['rank-selection']; const rank = selected_option.value; const messages = { "SPECIAL": `*Battle Special*: Wasn't expecting to see ya here <@${ctx.body.user.id}>... I guess you're up for a challenge huh? Alright then, who do ya wanna annoy today?`, "BEGINNER": `*Battle Beginner*: Hey there <@${ctx.body.user.id}>! Let's keep things simple, who do you want to battle against?`, "CASUAL": `*Battle Casual*: Alright <@${ctx.body.user.id}>, things are about to get a little bit tougher from here... Who do you feel like taking on today?` } const canRankUp = (() => { if (user.rank == 'Beginner') { return (user.health + user.mindmg + user.maxdmg) >= 50 && user.cshards >= 50 } else if (user.rank == 'Casual') { return (user.health + user.mindmg + user.maxdmg) >= 200 && user.cshards >= 250 } else { return false } })(); await ctx.ack({ response_action: 'update', view: { "private_metadata": ctx.payload.private_metadata, "type": "modal", "callback_id": "chooseopponent1", "title": { "type": "plain_text", "text": "Choose an opponent", "emoji": true }, "submit": { "type": "plain_text", "text": "Battle", "emoji": true }, "close": { "type": "plain_text", "text": "Never mind", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": messages[rank] || `*Battle Master*: Greetings <@${ctx.body.user.id}>. Please choose an opponent from below to battle.` } }, { "type": "divider" }, { "type": "input", "element": { "type": "static_select", "options": AllOpponents.filter(op => op.rank == rank).map(opponent => { const battlePower = ((opponent) => { if (opponent.stats.health instanceof Function) { return opponent.stats.health(user) + opponent.stats.min(user) + opponent.stats.max(user) } else { return opponent.stats.health + opponent.stats.min + opponent.stats.max } })(opponent); return { "text": { "type": "plain_text", "text": `${opponent.name} // ${battlePower} Battle Power` + (opponent.secretCondition && !opponent.secretCondition(user) ? " :lock:" : ""), "emoji": true }, "value": opponent.rawId } }), "action_id": "opponents" }, "label": { "type": "plain_text", "text": "Choose an opponent:", "emoji": true } }, ...(canRankUp ? [ { type: 'divider', }, { "type": "section", "text": { "type": "mrkdwn", "text": `You can also now participate in a rank up battle against *Battle ${user.rank}*. Click the button below to initiate the battle.` }, "accessory": { "type": "button", "text": { "type": "plain_text", "text": "Click Me", "emoji": true }, "value": ctx.context.userId, "action_id": "start-rankup-battle" } } ] : []) ] } }) }) app.view("chooseopponent1", async (ctx) => { await ctx.ack(); const channelId = ctx.view.private_metadata; const userId = ctx.context.userId; const slackUser = (await ctx.client.users.info({ user: userId })).user.profile; const opponent = AllOpponents.find(o => o.rawId == Object.values(ctx.payload.state.values)[0].opponents.selected_option.value); const player = await initializeUser(userId); if (opponent.secretCondition && !opponent.secretCondition(player)) { return await ctx.client.chat.postEphemeral({ channel: channelId, user: ctx.context.userId, text: `${slackUser.display_name_normalized} started a battle against ${opponent.name}.`, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: opponent.deniedIntro.replaceAll("{player}", slackUser.display_name_normalized) }, "accessory": { "type": "image", "image_url": opponent.image, "alt_text": opponent.name } } ] }) } const opponentStats = ((opponent) => { if (opponent.stats.health instanceof Function) { return [opponent.stats.health(player), opponent.stats.min(player), opponent.stats.max(player)] } else { return [opponent.stats.health, opponent.stats.min, opponent.stats.max] } })(opponent); await sql`UPDATE users SET playerhealth = ${player.health}, playermin = ${player.mindmg}, playermax = ${player.maxdmg}, currentOpponent = ${opponent.rawId}, opponenthealth = ${opponentStats[0]}, opponentmin = ${opponentStats[1]}, opponentmax = ${opponentStats[2]} WHERE slack_id = ${userId};` const msg = await ctx.client.chat.postMessage({ channel: channelId, text: `${slackUser.display_name_normalized} started a battle against ${opponent.name}.`, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: opponent.intro.replaceAll("{player}", slackUser.display_name_normalized) }, "accessory": { "type": "image", "image_url": opponent.image, "alt_text": opponent.name } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Continue", "emoji": true }, "value": userId, "action_id": "continue" } ] }, { type: 'image', title: { type: 'plain_text', text: `${slackUser.display_name_normalized} vs ${opponent.name}` }, image_url: await getBattlerUrl(userId, "battlesquadfight.png") + `&username=${encodeURIComponent(slackUser.display_name_normalized)}&opponent=${opponent.rawId}` }, ...(opponent.battleAwareness ? [{ type: 'section', text: { type: 'mrkdwn', text: `Watch out, ${slackUser.display_name_normalized}! ${opponent.name} has ${opponent.battleAwareness.type} Battle Awareness! This means they will react to some or ALL of your actions. Be careful!` }, "accessory": { "type": "image", "image_url": "", "alt_text": `${opponent.battleAwareness.type} Battle Awareness` } }] : []) ] }); await sql`UPDATE users SET battlemessage = ${`https://hackclub.slack.com/archives/${channelId}/p${msg.ts.replace('.', '')}`} WHERE slack_id = ${userId};` }) async function checkButton(ctx) { await ctx.ack(); if (ctx.payload.value != ctx.context.userId) { return ctx.respond({ replace_original: false, response_type: 'ephemeral', text: "Battle Support: Please do not click other battlers buttons!", blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Support:* Erm, <@${ctx.context.userId}>?... Please don't press other battlers buttons! If you want to choose an opponent yourself, simply use \`/chooseopponent\` to get started!` } } ] }) } await ctx.next(); } function chooseAction(chances) { const random = Math.random(); let cumulative = 0; for (const action in chances) { cumulative += chances[action]; if (random < cumulative) { return action; } } } async function takeOutOfBattle(slack_id) { return await sql`UPDATE users SET currentopponent = 'None', playerhealth = NULL, playermin = NULL, playermax = NULL, playerdefense = NULL, playerdefendcount = 0, opponenthealth = NULL, opponentmin = NULL, opponentmax = NULL, opponentdefense = NULL, opponentdefendcount = 0, battlemessage = NULL WHERE slack_id = ${slack_id}; ` } async function playerLoss(ctx) { const user = await initializeUser(ctx.context.userId); const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; await takeOutOfBattle(ctx.context.userId); const special = !!SpecialOpponents.find(x => x.rawId == user.currentopponent); if (special) { await sql` UPDATE users losses = ${user.losses + 1} WHERE slack_id = ${ctx.context.userId}; ` } else { await sql` UPDATE users SET spoints = ${user.spoints - 1}, cshards = ${user.cshards - 5}, losses = ${user.losses + 1} WHERE slack_id = ${ctx.context.userId}; ` } ctx.respond({ replace_original: true, text: `${slackUser.display_name_normalized} lost against ${AllOpponents.find(x => x.rawId == user.currentopponent).name}`, "blocks": [ { "type": "context", "elements": [ { "type": "mrkdwn", "text": "*DEFEAT :trophy:*" } ] }, { "type": "section", "text": { "type": "mrkdwn", "text": `Oh no ${slackUser.display_name_normalized}... You've been defeated by ${AllOpponents.find(x => x.rawId == user.currentopponent).name}... Don't give up! There's always room for improvement!\n\n\n*Losses:*\n> ${special ? 0 : -1} Skill Point(s)\n>${special ? 0 : -5} Creation Shards\n> 1 Defeat` }, "accessory": { "type": "image", "image_url": AllOpponents.find(x => x.rawId == user.currentopponent).image, "alt_text": AllOpponents.find(x => x.rawId == user.currentopponent).name } } ] }) } async function playerWin(ctx) { const user = await initializeUser(ctx.context.userId); const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; await takeOutOfBattle(ctx.context.userId); await sql` UPDATE users SET spoints = ${user.spoints + 1}, cshards = ${user.cshards + 5}, victories = ${user.victories + 1} WHERE slack_id = ${ctx.context.userId}; ` ctx.respond({ replace_original: true, text: `${slackUser.display_name_normalized} won against ${AllOpponents.find(x => x.rawId == user.currentopponent).name}`, "blocks": [ { "type": "context", "elements": [ { "type": "mrkdwn", "text": "*YOU WON! :trophy:*" } ] }, { "type": "section", "text": { "type": "mrkdwn", "text": `Congratulations ${slackUser.display_name_normalized}! You have defeated ${AllOpponents.find(x => x.rawId == user.currentopponent).name} in battle! Good job!\n\n\n*Rewards:*\n> 1 Skill Point\n>5 Creation Shards\n> 1 Victory` }, "accessory": { "type": "image", "image_url": AllOpponents.find(x => x.rawId == user.currentopponent).image, "alt_text": AllOpponents.find(x => x.rawId == user.currentopponent).name } } ] }) } app.action("continue", checkButton, async (ctx) => { const user = await initializeUser(ctx.context.userId); const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; if (user.playerhealth <= 0) { return playerLoss(ctx) } ctx.respond({ replace_original: true, text: "", "blocks": [ { "type": "section", "fields": [ { "type": "mrkdwn", "text": `${slackUser.display_name_normalized}:\n\n*Health:* ${user.playerhealth}\n*Min Damage:* ${user.playermin}\n*Max Damage:* ${user.playermax}` }, { "type": "mrkdwn", "text": `${AllOpponents.find(x => x.rawId == user.currentopponent).name}:\n\n*Health:* ${user.opponenthealth}\n*Min Damage:* ${user.opponentmin}\n*Max Damage:* ${user.opponentmax}` } ], "accessory": { "type": "image", "image_url": AllOpponents.find(x => x.rawId == user.currentopponent).image, "alt_text": AllOpponents.find(x => x.rawId == user.currentopponent).name } }, { "type": "context", "elements": [ { "type": "mrkdwn", "text": `*${slackUser.display_name_normalized}* vs *${AllOpponents.find(x => x.rawId == user.currentopponent).name}* | Your Turn` } ] }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "⚔️ Attack", "emoji": true }, "value": ctx.context.userId, "action_id": "attack" }, { "type": "button", "text": { "type": "plain_text", "text": "🛡️ Defend", "emoji": true }, "value": ctx.context.userId, "action_id": "defend" }, { "type": "button", "text": { "type": "plain_text", "text": " 🎁 Item", "emoji": true }, "value": ctx.context.userId, "action_id": "item" }, { "type": "button", "text": { "type": "plain_text", "text": "☠️ Forfeit", "emoji": true }, "style": "danger", "value": ctx.context.userId, "action_id": "forfeit" } ] } ] }) }) app.action('forfeit', checkButton, async (ctx) => { ctx.respond({ replace_original: false, response_type: 'ephemeral', text: 'Not yet...', }) }) app.action(/attack|defend|item/, checkButton, async (ctx) => { let response = "This error message isn't meant to show up. If it does, contact Haroon."; const user = await initializeUser(ctx.context.userId); const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; await sql`UPDATE users SET lastaction = ${ctx.payload.action_id} WHERE slack_id = ${ctx.context.userId};`; switch (ctx.payload.action_id) { case 'attack': if (user.opponentdefense == 'Strong') { response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`NO DAMAGE\`\`\`` } else if (user.opponentdefense == 'Moderate') { if (Math.random() < 0.5) { response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`NO DAMAGE\`\`\`` } else { const damage = Math.floor(Math.random() * (user.playermax - user.playermin + 1)) + user.playermin; await sql`UPDATE users SET opponenthealth = ${user.opponenthealth - damage} WHERE slack_id = ${ctx.context.userId};` response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\`` } } else if (user.opponentdefense == 'Weak') { if (Math.random() < 0.25) { response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`NO DAMAGE\`\`\`` } else { const damage = Math.floor(Math.random() * (user.playermax - user.playermin + 1)) + user.playermin; await sql`UPDATE users SET opponenthealth = ${user.opponenthealth - damage} WHERE slack_id = ${ctx.context.userId};` response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\`` } } else { const damage = Math.floor(Math.random() * (user.playermax - user.playermin + 1)) + user.playermin; await sql`UPDATE users SET opponenthealth = ${user.opponenthealth - damage} WHERE slack_id = ${ctx.context.userId};` response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\`` } break; case 'defend': if (user.playerdefendcount < 3) { await sql`UPDATE users SET playerdefense = 'Strong' WHERE slack_id = ${ctx.context.userId};` response = `*_A blue forcefield magically appears around ${slackUser.display_name_normalized}_*\n\n\`\`\`STRONG DEFENCE\`\`\`` } else { const type = ["Strong", "Moderate", "Weak"][Math.floor(Math.random() * 3)]; await sql`UPDATE users SET playerdefense = ${type} WHERE slack_id = ${ctx.context.userId};` response = `*_A blue forcefield magically appears around ${slackUser.display_name_normalized}_*\n\n\`\`\`${type.toUpperCase()} DEFENCE\`\`\`` } await sql`UPDATE users SET playerdefendcount = ${user.opponentdefendcount + 1} WHERE slack_id = ${ctx.context.userId};` break; case 'item': const increase = Math.floor(Math.random() * 5) + 1; switch (['health', 'min', 'max', 'nothing'][Math.floor(Math.random() * 4)]) { case 'health': await sql`UPDATE users SET playerhealth = ${user.playerhealth + increase} WHERE slack_id = ${ctx.context.userId};` response = `*_${slackUser.display_name_normalized} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} HEALTH\`\`\`` break; case 'min': if ((user.playermin + increase) < user.playermax) { response = `*_${slackUser.display_name_normalized} touches some grass_*\n\n\`\`\`NOTHING HAPPENED\`\`\`` break; } await sql`UPDATE users SET playermin = ${user.playermin + increase} WHERE slack_id = ${ctx.context.userId};` response = `*_${slackUser.display_name_normalized} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} MIN DAMAGE\`\`\`` break; case 'max': await sql`UPDATE users SET playermax = ${user.playermax + increase} WHERE slack_id = ${ctx.context.userId};` response = `*_${slackUser.display_name_normalized} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} MAX DAMAGE\`\`\`` break; case 'nothing': response = `*_${slackUser.display_name_normalized} touches some grass_*\n\n\`\`\`NOTHING HAPPENED\`\`\`` break; } } await sql`UPDATE users SET opponentdefense = 'None' WHERE slack_id = ${ctx.context.userId};` await ctx.respond({ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": response } }, { type: 'image', image_url: `https://generator.battlemaster.obl.ong/playeraction.png?action=${ctx.payload.action_id.charAt(0).toUpperCase() + ctx.payload.action_id.slice(1)}+Left&player=${encodeURIComponent(await getBattlerUrl(ctx.context.userId))}`, alt_text: ctx.payload.action_id.charAt(0).toUpperCase() }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Continue", "emoji": true }, "value": ctx.context.userId, "action_id": "continue-opponent" } ] } ] }) }) app.action("continue-opponent", checkButton, async (ctx) => { const user = await initializeUser(ctx.context.userId); const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; if (user.opponenthealth <= 0) { return playerWin(ctx) } ctx.respond({ replace_original: true, text: "", "blocks": [ { "type": "section", "fields": [ { "type": "mrkdwn", "text": `${slackUser.display_name_normalized}:\n\n*Health:* ${user.playerhealth}\n*Min Damage:* ${user.playermin}\n*Max Damage:* ${user.playermax}` }, { "type": "mrkdwn", "text": `${AllOpponents.find(x => x.rawId == user.currentopponent).name}:\n\n*Health:* ${user.opponenthealth}\n*Min Damage:* ${user.opponentmin}\n*Max Damage:* ${user.opponentmax}` } ], "accessory": { "type": "image", "image_url": AllOpponents.find(x => x.rawId == user.currentopponent).image, "alt_text": AllOpponents.find(x => x.rawId == user.currentopponent).name } }, { "type": "context", "elements": [ { "type": "mrkdwn", "text": `*${slackUser.display_name_normalized}* vs *${AllOpponents.find(x => x.rawId == user.currentopponent).name}* | ${AllOpponents.find(x => x.rawId == user.currentopponent).name}'s Turn` } ] }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Continue", "emoji": true }, "value": ctx.context.userId, "action_id": "viewaction-opponent" } ] } ] }) }) app.action('viewaction-opponent', checkButton, async (ctx) => { let response = "This error message isn't meant to show up. If it does, contact Haroon."; const user = await initializeUser(ctx.context.userId); const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; const opponent = AllOpponents.find(x => x.rawId == user.currentopponent); const action = (() => { if (opponent.battleAwareness && opponent.battleAwareness.chances[user.lastaction]) { return chooseAction(opponent.battleAwareness.chances[user.lastaction](user)); } return chooseAction(opponent.chances); })(); switch (action) { case 'attack': if (user.playerdefense == 'Strong') { response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} attacks ${slackUser.display_name_normalized}_*\n\n\`\`\`NO DAMAGE\`\`\`` } else if (user.playerdefense == 'Moderate') { if (Math.random() < 0.5) { response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} attacks ${slackUser.display_name_normalized}_*\n\n\`\`\`NO DAMAGE\`\`\`` } else { const damage = Math.floor(Math.random() * (user.opponentmax - user.opponentmin + 1)) + user.opponentmin; await sql`UPDATE users SET playerhealth = ${user.playerhealth - damage} WHERE slack_id = ${ctx.context.userId};` response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} attacks ${slackUser.display_name_normalized}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\`` } } else if (user.playerdefense == 'Weak') { if (Math.random() < 0.25) { response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} attacks ${lackUser.display_name_normalized}_*\n\n\`\`\`NO DAMAGE\`\`\`` } else { const damage = Math.floor(Math.random() * (user.opponentmax - user.opponentmin + 1)) + user.opponentmin; await sql`UPDATE users SET playerhealth = ${user.playerhealth - damage} WHERE slack_id = ${ctx.context.userId};` response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} attacks ${slackUser.display_name_normalized}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\`` } } else { const damage = Math.floor(Math.random() * (user.opponentmax - user.opponentmin + 1)) + user.opponentmin; await sql`UPDATE users SET playerhealth = ${user.playerhealth - damage} WHERE slack_id = ${ctx.context.userId};` response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} attacks ${slackUser.display_name_normalized}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\`` } break; case 'defend': if (user.opponentdefendcount < 3) { await sql`UPDATE users SET opponentdefense = 'Strong' WHERE slack_id = ${ctx.context.userId};` response = `*_A blue forcefield magically appears around ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`STRONG DEFENCE\`\`\`` } else { const type = ["Strong", "Moderate", "Weak"][Math.floor(Math.random() * 3)]; await sql`UPDATE users SET opponentdefense = ${type} WHERE slack_id = ${ctx.context.userId};` response = `*_A blue forcefield magically appears around ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`${type.toUpperCase()} DEFENCE\`\`\`` } await sql`UPDATE users SET opponentdefendcount = ${user.opponentdefendcount + 1} WHERE slack_id = ${ctx.context.userId};` break; case 'item': const increase = Math.floor(Math.random() * 5) + 1; switch (['health', 'health', 'min', 'min', 'max', 'nothing', 'nothing', 'nothing'][Math.floor(Math.random() * 8)]) { case 'health': await sql`UPDATE users SET opponenthealth = ${user.opponenthealth + increase} WHERE slack_id = ${ctx.context.userId};` response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} HEALTH\`\`\`` break; case 'min': if ((user.opponentmin + increase) < user.opponentmax) { response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} touches some grass_*\n\n\`\`\`NOTHING HAPPENED\`\`\`` break; } await sql`UPDATE users SET opponentmin = ${user.opponentmin + increase} WHERE slack_id = ${ctx.context.userId};` response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} MIN DAMAGE\`\`\`` break; case 'max': await sql`UPDATE users SET opponentmax = ${user.opponentmax + increase} WHERE slack_id = ${ctx.context.userId};` response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} MAX DAMAGE\`\`\`` break; case 'nothing': response = `*_${AllOpponents.find(x => x.rawId == user.currentopponent).name} touches some grass_*\n\n\`\`\`NOTHING HAPPENED\`\`\`` break; } } await sql`UPDATE users SET playerdefense = 'None' WHERE slack_id = ${ctx.context.userId};` await ctx.respond({ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": response } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Continue", "emoji": true }, "value": ctx.context.userId, "action_id": "continue" } ] } ] }) }) app.command('/bm-eval', async (ctx) => { await ctx.ack(); if (ctx.context.userId != 'U06TBP41C3E') return; const resp = require('util').inspect(await eval(ctx.body.text), undefined, 1) ctx.respond({ text: resp, response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: "```" + resp + "```" } } ] }) }) app.command('/viewopponents', async (ctx) => { await ctx.ack(); const args = ctx.body.text.slice().split(/ +/g); switch (args[0].toUpperCase()) { case "SPECIAL": ctx.respond({ text: "You're trying to view Special opponents.", response_type: 'ephemeral' }) break; case "BEGINNER": const mappedBeginner = BeginnerOpponents.map(opponent => ({ name: opponent.name, battlePower: opponent.stats.health + opponent.stats.min + opponent.stats.max }) ) ctx.respond({ response_type: 'ephemeral', text: `*Battle Master:* Greetings battler. Here are the avaliable *Beginner* opponents for you to battle.`, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Greetings <@${ctx.body.user_id}>. Here are the avaliable *Beginner* opponents for you to battle.` } }, { type: 'section', fields: mappedBeginner.map(opponent => ({ type: 'mrkdwn', text: `*${opponent.name}* // ${opponent.battlePower} Battle Power` })) } ] }) break; case "CASUAL": ctx.say("You're trying to view Casual opponents.") break; default: ctx.say("You either wrote nothing or just chose a rank that doesn't exist.") } }); function getTimeDifference(date1, date2) { // Ensure both dates are Date objects const start = new Date(date1); const end = new Date(date2); // Calculate the difference in milliseconds const diff = Math.abs(end - start); // Calculate the difference in weeks, days, hours, minutes, and seconds const weeks = Math.floor(diff / (1000 * 60 * 60 * 24 * 7)); const days = Math.floor(diff / (1000 * 60 * 60 * 24)) % 7; const hours = Math.floor(diff / (1000 * 60 * 60)) % 24; const minutes = Math.floor(diff / (1000 * 60)) % 60; const seconds = Math.floor(diff / 1000) % 60; // Determine which unit to use for the output if (weeks > 0) { return `${weeks} Week${weeks > 1 ? 's' : ''}`; } else if (days > 0) { return `${days} Day${days > 1 ? 's' : ''}`; } else if (hours > 0) { return `${hours} Hour${hours > 1 ? 's' : ''}`; } else if (minutes > 0) { return `${minutes} Minute${minutes > 1 ? 's' : ''}`; } else { return `${seconds} Second${seconds > 1 ? 's' : ''}`; } } app.command('/daily', async (ctx) => { await ctx.ack(); const [cooldown] = await sql`SELECT * FROM cooldowns WHERE slack_id = ${ctx.context.userId};` const user = await initializeUser(ctx.context.userId) const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; const now = new Date(); if (!cooldown.daily || (cooldown.daily.getTime() <= now.getTime())) { const cshards = Math.floor(Math.random() * 10) + 1; const spoints = Math.floor(Math.random() * 5) + 1; ctx.respond({ response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Oh ${slackUser.display_name_normalized}, apologies for the wait. Here is your reward for today.\n\n> ${cshards} Creation Shards\n> ${spoints} Skill Points` } } ] }) await sql`UPDATE users SET cshards = ${user.cshards + cshards}, spoints = ${user.spoints + spoints} WHERE slack_id = ${ctx.context.userId};` await sql`UPDATE cooldowns SET daily = ${new Date(now.getTime() + (24 * 60 * 60 * 1000))};` } else { ctx.respond({ response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Apologies battler. Unfortunately you must wait *${getTimeDifference(now, cooldown.daily)}* before you can collect your next daily reward.` } } ] }) } }) app.command('/weekly', async (ctx) => { await ctx.ack(); const [cooldown] = await sql`SELECT * FROM cooldowns WHERE slack_id = ${ctx.context.userId};` const user = await initializeUser(ctx.context.userId) const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; const now = new Date(); if (!cooldown.weekly || (cooldown.weekly.getTime() <= now.getTime())) { const cshards = Math.floor(Math.random() * 20) + 1; const dshards = Math.floor(Math.random() * 15) + 1; const spoints = Math.floor(Math.random() * 10) + 1; ctx.respond({ response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* It's that time again ${slackUser.display_name_normalized}. Enjoy your weekly reward.\n\n> ${cshards} Creation Shards\n> ${dshards} Destruction Shards\n> ${spoints} Skill Points` } } ] }) await sql`UPDATE users SET cshards = ${user.cshards + cshards}, dshards = ${user.dshards + dshards}, spoints = ${user.spoints + spoints} WHERE slack_id = ${ctx.context.userId};` await sql`UPDATE cooldowns SET weekly = ${new Date(now.getTime() + (7 * 24 * 60 * 60 * 1000))};` } else { ctx.respond({ response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Apologies battler. Unfortunately you must wait *${getTimeDifference(now, cooldown.weekly)}* before you can collect your next weekly reward.` } } ] }) } }) app.command('/monthly', async (ctx) => { await ctx.ack(); const [cooldown] = await sql`SELECT * FROM cooldowns WHERE slack_id = ${ctx.context.userId};` const user = await initializeUser(ctx.context.userId) const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile; const now = new Date(); if (!cooldown.monthly || (cooldown.monthly.getTime() <= now.getTime())) { const cshards = Math.floor(Math.random() * 30) + 1; const dshards = Math.floor(Math.random() * 25) + 1; const spoints = Math.floor(Math.random() * 15) + 1; ctx.respond({ response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Another month, another reward. You have my appreciation ${slackUser.display_name_normalized}.\n\n> ${cshards} Creation Shards\n> ${dshards} Destruction Shards\n> ${spoints} Skill Points` } } ] }) await sql`UPDATE users SET cshards = ${user.cshards + cshards}, dshards = ${user.dshards + dshards}, spoints = ${user.spoints + spoints} WHERE slack_id = ${ctx.context.userId};` await sql`UPDATE cooldowns SET monthly = ${new Date(now.getTime() + (30 * 24 * 60 * 60 * 1000))};` } else { ctx.respond({ response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Apologies battler. Unfortunately you must wait *${getTimeDifference(now, cooldown.monthly)}* before you can collect your next monthly reward.` } } ] }) } }) app.command('/upgrade', async (ctx) => { await ctx.ack(); const user = await initializeUser(ctx.context.userId); await ctx.client.views.open({ trigger_id: ctx.body.trigger_id, view: { "private_metadata": ctx.payload.channel_id, "type": "modal", "callback_id": "upgrade", "title": { "type": "plain_text", "text": "Upgrade your stats", "emoji": true }, "submit": { "type": "plain_text", "text": "Upgrade", "emoji": true }, "close": { "type": "plain_text", "text": "Never mind", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `*Battle Builder:* Helloooo <@${ctx.context.userId}>! You're here to upgrade yourself right? No problem! What would you like to... Erm... Upgrade?-` } }, { "block_id": "select", "type": "input", "element": { "type": "static_select", "placeholder": { "type": "plain_text", "text": "Choose a statistic...", "emoji": true }, "options": [ { "text": { "type": "plain_text", "text": "Health // 5 Skill Points" + (user.spoints < 5 ? " :lock:" : ""), "emoji": true }, "value": "health" }, { "text": { "type": "plain_text", "text": "Min Damage // 10 Skill Points" + (user.spoints < 10 || user.mindmg + 5 == user.maxdmg ? " :lock:" : ""), "emoji": true }, "value": "mindmg" }, { "text": { "type": "plain_text", "text": "Max Damage // 15 Skill Points" + (user.spoints < 15 ? " :lock:" : ""), "emoji": true }, "value": "maxdmg" } ], "action_id": "upgrade-modal" }, "label": { "type": "plain_text", "text": "What would you like to upgrade?", "emoji": true } } ] } }) }); app.view('upgrade', async (ctx) => { const user = await initializeUser(ctx.context.userId); const { selected_option } = ctx.view.state.values['select']['upgrade-modal']; if (selected_option.value == "health" && user.spoints < 5) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You don't have enough skill points to upgrade your health!" } }); } else if (selected_option.value == "mindmg" && user.spoints < 10) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You don't have enough skill points to upgrade your minimum damage!" } }); } else if (selected_option == "mindmg" && user.mindmg + 5 == user.maxdmg) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You need to upgrade your maximum damage first!" } }); } else if (selected_option.value == "maxdmg" && user.spoints < 15) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You don't have enough skill points to upgrade your maximum damage!" } }); } await ctx.ack(); if (selected_option.value == "health") { await sql`UPDATE users SET spoints = ${user.spoints - 5}, health = ${user.health + 1} WHERE slack_id = ${ctx.context.userId};` await ctx.client.chat.postEphemeral({ channel: ctx.view.private_metadata, user: ctx.context.userId, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Builder:* Yayyy <@${ctx.context.userId}>! Your health has been successfully increased!\n\n\`\`\`HEALTH INCREASED (${user.health} > ${user.health + 1})\`\`\`` } } ] }) } else if (selected_option.value == "mindmg") { await sql`UPDATE users SET spoints = ${user.spoints - 10}, mindmg = ${user.mindmg + 1} WHERE slack_id = ${ctx.context.userId};` await ctx.client.chat.postEphemeral({ channel: ctx.view.private_metadata, user: ctx.context.userId, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Builder:* Yayyy <@${ctx.context.userId}>! Your minimum damage has been successfully increased!\n\n\`\`\`MIN DAMAGE INCREASED (${user.mindmg} > ${user.mindmg + 1})\`\`\`` } } ] }) } else if (selected_option.value == "maxdmg") { await sql`UPDATE users SET spoints = ${user.spoints - 15}, maxdmg = ${user.maxdmg + 1} WHERE slack_id = ${ctx.context.userId};` await ctx.client.chat.postEphemeral({ channel: ctx.view.private_metadata, user: ctx.context.userId, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Builder:* Yayyy <@${ctx.context.userId}>! Your maximum damage has been successfully increased!\n\n\`\`\`MAX DAMAGE INCREASED (${user.maxdmg} > ${user.maxdmg + 1})\`\`\`` } } ] }) } }) app.command('/downgrade', async (ctx) => { await ctx.ack(); const user = await initializeUser(ctx.context.userId); await ctx.client.views.open({ trigger_id: ctx.body.trigger_id, view: { "private_metadata": ctx.payload.channel_id, "type": "modal", "callback_id": "downgrade", "title": { "type": "plain_text", "text": "Downgrade your stats", "emoji": true }, "submit": { "type": "plain_text", "text": "Downgrade", "emoji": true }, "close": { "type": "plain_text", "text": "Never mind", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `*Battle Builder:* Helloooo <@${ctx.context.userId}>! You're downgrading today huh? No problem! What would you like to... Erm... Downgrade?-` } }, { "block_id": "select", "type": "input", "element": { "type": "static_select", "placeholder": { "type": "plain_text", "text": "Choose a statistic...", "emoji": true }, "options": [ { "text": { "type": "plain_text", "text": "Health // 30 Creation Shards" + (user.cshards < 30 ? " :lock:" : ""), "emoji": true }, "value": "health" }, { "text": { "type": "plain_text", "text": "Min Damage // 40 Creation Shards" + (user.cshards < 40 ? " :lock:" : ""), "emoji": true }, "value": "mindmg" }, { "text": { "type": "plain_text", "text": "Max Damage // 50 Creation Shards" + (user.cshards < 50 || user.mindmg + 5 == user.maxdmg ? " :lock:" : ""), "emoji": true }, "value": "maxdmg" } ], "action_id": "downgrade-modal" }, "label": { "type": "plain_text", "text": "What would you like to downgrade?", "emoji": true } } ] } }) }); app.view('downgrade', async (ctx) => { const user = await initializeUser(ctx.context.userId); const { selected_option } = ctx.view.state.values['select']['downgrade-modal']; if (selected_option.value == "health" && user.cshards < 30) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You don't have enough creation shards to downgrade your health!" } }); } else if (selected_option.value == "mindmg" && user.cshards < 40) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You don't have enough creation shards to downgrade your minimum damage!" } }); } else if (selected_option == "maxdmg" && user.mindmg + 5 == user.maxdmg) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You need to downgrade your minimum damage first!" } }); } else if (selected_option.value == "maxdmg" && user.cshards < 50) { return await ctx.ack({ response_action: 'errors', errors: { 'select': "You don't have enough creation shards to downgrade your maximum damage!" } }); } await ctx.ack(); if (selected_option.value == "health") { await sql`UPDATE users SET spoints = ${user.spoints + 5}, health = ${user.health - 1}, cshards = ${user.cshards - 30} WHERE slack_id = ${ctx.context.userId};` await ctx.client.chat.postEphemeral({ channel: ctx.view.private_metadata, user: ctx.context.userId, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Builder:* Yayyy <@${ctx.context.userId}>! Your health has been successfully decreased!\n\n\`\`\`HEALTH DECREASED (${user.health} > ${user.health - 1})\`\`\`` } } ] }) } else if (selected_option.value == "mindmg") { await sql`UPDATE users SET spoints = ${user.spoints + 10}, mindmg = ${user.mindmg - 1}, cshards = ${user.cshards - 40} WHERE slack_id = ${ctx.context.userId};` await ctx.client.chat.postEphemeral({ channel: ctx.view.private_metadata, user: ctx.context.userId, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Builder:* Yayyy <@${ctx.context.userId}>! Your minimum damage has been successfully decreased!\n\n\`\`\`MIN DAMAGE DECREASED (${user.mindmg} > ${user.mindmg - 1})\`\`\`` } } ] }) } else if (selected_option.value == "maxdmg") { await sql`UPDATE users SET spoints = ${user.spoints + 15}, maxdmg = ${user.maxdmg - 1}, cshards = ${user.cshards - 50} WHERE slack_id = ${ctx.context.userId};` await ctx.client.chat.postEphemeral({ channel: ctx.view.private_metadata, user: ctx.context.userId, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Builder:* Yayyy <@${ctx.context.userId}>! Your maximum damage has been successfully decreased!\n\n\`\`\`MAX DAMAGE DECREASED (${user.maxdmg} > ${user.maxdmg - 1})\`\`\`` } } ] }) } }) ; (async () => { await app.start(process.env.PORT); console.log('⚡️ Bolt app is running!'); })();