|
1 | | -const { MessageFlags, userMention, channelMention, bold } = require("discord.js"); |
| 1 | +const { MessageFlags, userMention, channelMention, bold, ModalBuilder, LabelBuilder, UserSelectMenuBuilder, StringSelectMenuBuilder } = require("discord.js"); |
2 | 2 | const { timeConversion } = require("../../../shared"); |
3 | | -const { commandMention, bountyEmbed, goalCompletionEmbed, sendRewardMessage, reloadHunterMapSubset, syncRankRoles, unarchiveAndUnlockThread, rewardSummary, consolidateHunterReceipts, refreshReferenceChannelScoreboardSeasonal, refreshReferenceChannelScoreboardOverall } = require("../../shared"); |
| 3 | +const { commandMention, bountyEmbed, goalCompletionEmbed, sendRewardMessage, syncRankRoles, unarchiveAndUnlockThread, rewardSummary, consolidateHunterReceipts, refreshReferenceChannelScoreboardSeasonal, refreshReferenceChannelScoreboardOverall, sentenceListEN, butIgnoreInteractionCollectorErrors, selectOptionsFromBounties } = require("../../shared"); |
4 | 4 | const { SubcommandWrapper } = require("../../classes"); |
5 | 5 | const { Company } = require("../../../database/models"); |
| 6 | +const { SKIP_INTERACTION_HANDLING } = require("../../../constants"); |
6 | 7 |
|
7 | 8 | module.exports = new SubcommandWrapper("complete", "Close one of your open bounties, distributing rewards to hunters who turned it in", |
8 | 9 | async function executeSubcommand(interaction, origin, runMode, logicLayer) { |
9 | | - const slotNumber = interaction.options.getInteger("bounty-slot"); |
10 | | - const bounty = await logicLayer.bounties.findBounty({ userId: interaction.user.id, slotNumber, companyId: interaction.guild.id }); |
11 | | - if (!bounty) { |
12 | | - interaction.reply({ content: "You don't have a bounty in the `bounty-slot` provided.", flags: MessageFlags.Ephemeral }); |
| 10 | + const openBounties = await logicLayer.bounties.findOpenBounties(origin.user.id, origin.company.id); |
| 11 | + if (openBounties.length < 1) { |
| 12 | + interaction.reply({ content: "You don't appear to have any open bounties in this server.", flags: MessageFlags.Ephemeral }); |
| 13 | + return; |
| 14 | + } |
| 15 | + |
| 16 | + const labelIdBountyId = "bounty-id"; |
| 17 | + const labelIdBountyHunters = "hunters"; |
| 18 | + const maxHunters = 10; |
| 19 | + const modal = new ModalBuilder().setCustomId(`${SKIP_INTERACTION_HANDLING}${interaction.id}`) |
| 20 | + .setTitle("Mark your Bounty Complete!") |
| 21 | + .addLabelComponents( |
| 22 | + new LabelBuilder().setLabel("Bounty") |
| 23 | + .setStringSelectMenuComponent( |
| 24 | + new StringSelectMenuBuilder().setCustomId(labelIdBountyId) |
| 25 | + .setPlaceholder("Select a bounty...") |
| 26 | + .setOptions(selectOptionsFromBounties(openBounties)) |
| 27 | + ), |
| 28 | + new LabelBuilder().setLabel("Extra Turn-Ins") |
| 29 | + .setUserSelectMenuComponent( |
| 30 | + new UserSelectMenuBuilder().setCustomId(labelIdBountyHunters) |
| 31 | + .setPlaceholder(`Select up to ${maxHunters} bounty hunters...`) |
| 32 | + .setMaxValues(maxHunters) |
| 33 | + .setRequired(false) |
| 34 | + ) |
| 35 | + ); |
| 36 | + await interaction.showModal(modal); |
| 37 | + const modalSubmission = await interaction.awaitModalSubmit({ filter: incoming => incoming.customId === modal.data.custom_id, time: timeConversion(5, "m", "ms") }) |
| 38 | + .catch(butIgnoreInteractionCollectorErrors); |
| 39 | + if (!modalSubmission) { |
| 40 | + return; |
| 41 | + } |
| 42 | + |
| 43 | + const bounty = await logicLayer.bounties.findBounty(modalSubmission.fields.getStringSelectValues(labelIdBountyId)[0]); |
| 44 | + if (bounty?.state !== "open") { |
| 45 | + modalSubmission.reply({ content: "Your selected bounty no longer appears to be open.", flags: MessageFlags.Ephemeral }); |
13 | 46 | return; |
14 | 47 | } |
15 | 48 |
|
16 | 49 | // disallow completion within 5 minutes of creating bounty |
17 | 50 | if (runMode === "production" && new Date() < new Date(new Date(bounty.createdAt) + timeConversion(5, "m", "ms"))) { |
18 | | - interaction.reply({ content: `Bounties cannot be completed within 5 minutes of their posting. You can ${commandMention("bounty add-completers")} so you won't forget instead.`, flags: MessageFlags.Ephemeral }); |
| 51 | + modalSubmission.reply({ content: `Bounties cannot be completed within 5 minutes of their posting. You can ${commandMention("bounty record-turn-ins")} so you won't forget instead.`, flags: MessageFlags.Ephemeral }); |
19 | 52 | return; |
20 | 53 | } |
21 | 54 |
|
| 55 | + // Early-out if no completers |
22 | 56 | const completions = await logicLayer.bounties.findBountyCompletions(bounty.id); |
23 | | - const hunterCollection = await interaction.guild.members.fetch({ user: completions.map(reciept => reciept.userId) }); |
24 | | - for (const optionKey of ["first-bounty-hunter", "second-bounty-hunter", "third-bounty-hunter", "fourth-bounty-hunter", "fifth-bounty-hunter"]) { |
25 | | - const guildMember = interaction.options.getMember(optionKey); |
26 | | - if (guildMember) { |
27 | | - if (guildMember?.id !== interaction.user.id && !hunterCollection.has(guildMember.id)) { |
28 | | - hunterCollection.set(guildMember.id, guildMember); |
29 | | - } |
30 | | - } |
31 | | - } |
32 | | - |
| 57 | + const memberCollection = await modalSubmission.guild.members.fetch({ user: completions.map(reciept => reciept.userId) }); |
33 | 58 | const validatedHunters = new Map(); |
34 | | - for (const [memberId, member] of hunterCollection) { |
| 59 | + for (const [memberId, member] of memberCollection) { |
35 | 60 | if (runMode !== "production" || !member.user.bot) { |
36 | | - const { hunter: [hunter] } = await logicLayer.hunters.findOrCreateBountyHunter(memberId, interaction.guild.id); |
| 61 | + const { hunter: [hunter] } = await logicLayer.hunters.findOrCreateBountyHunter(memberId, origin.company.id); |
37 | 62 | if (!hunter.isBanned) { |
38 | 63 | validatedHunters.set(memberId, hunter); |
39 | 64 | } |
40 | 65 | } |
41 | 66 | } |
42 | 67 |
|
| 68 | + const extraTurnIns = modalSubmission.fields.getSelectedMembers(labelIdBountyHunters); |
| 69 | + if (extraTurnIns !== null) { |
| 70 | + for (const [memberId, member] of extraTurnIns) { |
| 71 | + if (runMode !== "production" || !(member.user.bot || validatedHunters.has(memberId))) { |
| 72 | + const { hunter: [hunter] } = await logicLayer.hunters.findOrCreateBountyHunter(memberId, origin.company.id); |
| 73 | + if (!hunter.isBanned) { |
| 74 | + validatedHunters.set(memberId, hunter); |
| 75 | + } |
| 76 | + } |
| 77 | + } |
| 78 | + } |
43 | 79 | if (validatedHunters.size < 1) { |
44 | | - interaction.reply({ content: `No bounty hunters have turn-ins recorded for this bounty. If you'd like to close your bounty without distributng rewards, use ${commandMention("bounty take-down")}.`, flags: MessageFlags.Ephemeral }) |
| 80 | + modalSubmission.reply({ content: `There aren't any eligible pending turn-ins for this bounty. If you'd like to close your bounty without paying out rewards, use ${commandMention("bounty take-down")}.`, flags: MessageFlags.Ephemeral }) |
45 | 81 | return; |
46 | 82 | } |
47 | 83 |
|
48 | | - await interaction.deferReply(); |
| 84 | + await modalSubmission.deferReply(); |
49 | 85 |
|
50 | 86 | const season = await logicLayer.seasons.incrementSeasonStat(bounty.companyId, "bountiesCompleted"); |
51 | 87 |
|
52 | | - let hunterMap = await logicLayer.hunters.getCompanyHunterMap(interaction.guild.id); |
| 88 | + let hunterMap = await logicLayer.hunters.getCompanyHunterMap(origin.company.id); |
53 | 89 |
|
54 | 90 | const previousCompanyLevel = Company.getLevel(origin.company.getXP(hunterMap)); |
55 | 91 | const hunterReceipts = await logicLayer.bounties.completeBounty(bounty, origin.hunter, validatedHunters, season, origin.company); |
56 | 92 | const { companyReceipt, goalProgress } = await logicLayer.goals.progressGoal(origin.company, "bounties", origin.hunter, season); |
57 | | - companyReceipt.guildName = interaction.guild.name; |
58 | | - hunterMap = await reloadHunterMapSubset(hunterMap, [...validatedHunters.keys(), origin.hunter.userId]); |
| 93 | + companyReceipt.guildName = modalSubmission.guild.name; |
| 94 | + hunterMap = await logicLayer.hunters.getCompanyHunterMap(origin.company.id); |
59 | 95 | const currentCompanyLevel = Company.getLevel(origin.company.getXP(hunterMap)); |
60 | 96 | if (previousCompanyLevel < currentCompanyLevel) { |
61 | 97 | companyReceipt.levelUp = currentCompanyLevel; |
62 | 98 | } |
63 | | - const descendingRanks = await logicLayer.ranks.findAllRanks(interaction.guild.id); |
| 99 | + const descendingRanks = await logicLayer.ranks.findAllRanks(origin.company.id); |
64 | 100 | const participationMap = await logicLayer.seasons.getParticipationMap(season.id); |
65 | | - const seasonalHunterReceipts = await logicLayer.seasons.updatePlacementsAndRanks(participationMap, descendingRanks, await interaction.guild.roles.fetch()); |
66 | | - syncRankRoles(seasonalHunterReceipts, descendingRanks, interaction.guild.members); |
| 101 | + const seasonalHunterReceipts = await logicLayer.seasons.updatePlacementsAndRanks(participationMap, descendingRanks, await modalSubmission.guild.roles.fetch()); |
| 102 | + syncRankRoles(seasonalHunterReceipts, descendingRanks, modalSubmission.guild.members); |
67 | 103 | consolidateHunterReceipts(hunterReceipts, seasonalHunterReceipts); |
68 | | - const content = rewardSummary("bounty", companyReceipt, hunterReceipts, origin.company.maxSimBounties); |
| 104 | + const rewardMessageContent = rewardSummary("bounty", companyReceipt, hunterReceipts, origin.company.maxSimBounties); |
69 | 105 |
|
70 | 106 | const acknowledgeOptions = { content: `${userMention(bounty.userId)}'s bounty, ` }; |
71 | 107 | if (goalProgress.goalCompleted) { |
72 | 108 | acknowledgeOptions.embeds = [goalCompletionEmbed(goalProgress.contributorIds)]; |
73 | 109 | } |
74 | 110 |
|
75 | 111 | if (origin.company.bountyBoardId) { |
76 | | - const bountyBoard = await interaction.guild.channels.fetch(origin.company.bountyBoardId); |
| 112 | + acknowledgeOptions.content += `${channelMention(bounty.postingId)}, was completed!`; |
| 113 | + modalSubmission.editReply(acknowledgeOptions); |
| 114 | + const bountyBoard = await modalSubmission.guild.channels.fetch(origin.company.bountyBoardId); |
77 | 115 | bountyBoard.threads.fetch(bounty.postingId).then(async thread => { |
78 | 116 | await unarchiveAndUnlockThread(thread, "bounty complete"); |
79 | 117 | thread.setAppliedTags([origin.company.bountyBoardCompletedTagId]); |
80 | | - thread.send({ content, flags: MessageFlags.SuppressNotifications }); |
| 118 | + thread.send({ content: rewardMessageContent, flags: MessageFlags.SuppressNotifications }); |
81 | 119 | return thread.fetchStarterMessage(); |
82 | 120 | }).then(async posting => { |
83 | 121 | posting.edit({ |
84 | | - embeds: [bountyEmbed(bounty, interaction.member, origin.hunter.getLevel(origin.company.xpCoefficient), true, origin.company, new Set([...validatedHunters.keys()]), await bounty.getScheduledEvent(interaction.guild.scheduledEvents), goalProgress)], |
| 122 | + embeds: [bountyEmbed(bounty, modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), true, origin.company, new Set([...validatedHunters.keys()]), await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents), goalProgress)], |
85 | 123 | components: [] |
86 | 124 | }).then(() => { |
87 | 125 | posting.channel.setArchived(true, "bounty completed"); |
88 | 126 | }); |
89 | 127 | }); |
90 | | - acknowledgeOptions.content += `${channelMention(bounty.postingId)}, was completed!`; |
91 | | - interaction.editReply(acknowledgeOptions); |
92 | 128 | } else { |
93 | 129 | acknowledgeOptions.content += `${bold(bounty.title)}, was completed!`; |
94 | | - interaction.editReply(acknowledgeOptions).then(message => { |
95 | | - sendRewardMessage(message, content, `${bounty.title} Rewards`); |
| 130 | + modalSubmission.editReply(acknowledgeOptions).then(message => { |
| 131 | + sendRewardMessage(message, rewardMessageContent, `${bounty.title} Rewards`); |
96 | 132 | }) |
97 | 133 | } |
98 | 134 |
|
99 | 135 | if (origin.company.scoreboardIsSeasonal) { |
100 | | - refreshReferenceChannelScoreboardSeasonal(origin.company, interaction.guild, participationMap, descendingRanks, goalProgress); |
| 136 | + refreshReferenceChannelScoreboardSeasonal(origin.company, modalSubmission.guild, participationMap, descendingRanks, goalProgress); |
101 | 137 | } else { |
102 | | - refreshReferenceChannelScoreboardOverall(origin.company, interaction.guild, hunterMap, goalProgress); |
| 138 | + refreshReferenceChannelScoreboardOverall(origin.company, modalSubmission.guild, hunterMap, goalProgress); |
103 | 139 | } |
104 | 140 | } |
105 | | -).setOptions( |
106 | | - { |
107 | | - type: "Integer", |
108 | | - name: "bounty-slot", |
109 | | - description: "The slot number of your bounty", |
110 | | - required: true |
111 | | - }, |
112 | | - { |
113 | | - type: "User", |
114 | | - name: "first-bounty-hunter", |
115 | | - description: "A bounty hunter who turned in the bounty", |
116 | | - required: false |
117 | | - }, |
118 | | - { |
119 | | - type: "User", |
120 | | - name: "second-bounty-hunter", |
121 | | - description: "A bounty hunter who turned in the bounty", |
122 | | - required: false |
123 | | - }, |
124 | | - { |
125 | | - type: "User", |
126 | | - name: "third-bounty-hunter", |
127 | | - description: "A bounty hunter who turned in the bounty", |
128 | | - required: false |
129 | | - }, |
130 | | - { |
131 | | - type: "User", |
132 | | - name: "fourth-bounty-hunter", |
133 | | - description: "A bounty hunter who turned in the bounty", |
134 | | - required: false |
135 | | - }, |
136 | | - { |
137 | | - type: "User", |
138 | | - name: "fifth-bounty-hunter", |
139 | | - description: "A bounty hunter who completed the bounty", |
140 | | - required: false |
141 | | - } |
142 | 141 | ); |
0 commit comments