From 3364548ec38986cb22dc34b54f5a0c57ea9576da Mon Sep 17 00:00:00 2001 From: nwjgit <69014816+nwjgit@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:44:30 -0500 Subject: [PATCH] Chambers of Xeric QOL & fakemass (#5970) Co-authored-by: GC <30398469+gc@users.noreply.github.com> --- src/lib/combat_achievements/grandmaster.ts | 66 ++- src/lib/combat_achievements/master.ts | 21 +- src/lib/data/cox.ts | 376 +++++++++++++----- src/lib/data/similarItems.ts | 2 +- src/lib/party.ts | 2 +- src/lib/types/minions.ts | 2 + src/lib/util/repeatStoredTrip.ts | 19 +- src/mahoji/commands/raid.ts | 20 +- src/mahoji/commands/testpotato.ts | 21 +- .../lib/abstracted_commands/coxCommand.ts | 132 ++++-- src/tasks/minions/minigames/raidsActivity.ts | 95 ++++- 11 files changed, 584 insertions(+), 172 deletions(-) diff --git a/src/lib/combat_achievements/grandmaster.ts b/src/lib/combat_achievements/grandmaster.ts index 78656f988b..790f28e27c 100644 --- a/src/lib/combat_achievements/grandmaster.ts +++ b/src/lib/combat_achievements/grandmaster.ts @@ -47,8 +47,16 @@ export const grandmasterCombatAchievements: CombatAchievement[] = [ type: 'speed', monster: 'Chambers of Xeric', rng: { - chancePerKill: 55, - hasChance: 'Raids' + chancePerKill: 1, + hasChance: data => + (data.type === 'Raids' && + (data as RaidsOptions).users.length >= 5 && + !(data as RaidsOptions).isFakeMass && + data.duration < Time.Minute * 12.5 * (data.quantity ?? 1)) || + (data.type === 'Raids' && + (data as RaidsOptions).isFakeMass && + ((data as RaidsOptions).maxSizeInput ?? 0) >= 5 && + data.duration < Time.Minute * 12.5 * (data.quantity ?? 1)) } }, { @@ -70,8 +78,12 @@ export const grandmasterCombatAchievements: CombatAchievement[] = [ type: 'speed', monster: 'Chambers of Xeric', rng: { - chancePerKill: 55, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).users.length === 1 + chancePerKill: 1, + hasChance: data => + data.type === 'Raids' && + (data as RaidsOptions).users.length === 1 && + !(data as RaidsOptions).isFakeMass && + data.duration < Time.Minute * 17 * (data.quantity ?? 1) } }, { @@ -81,8 +93,16 @@ export const grandmasterCombatAchievements: CombatAchievement[] = [ type: 'speed', monster: 'Chambers of Xeric', rng: { - chancePerKill: 55, - hasChance: 'Raids' + chancePerKill: 1, + hasChance: data => + (data.type === 'Raids' && + (data as RaidsOptions).users.length >= 3 && + !(data as RaidsOptions).isFakeMass && + data.duration < Time.Minute * 14.5 * (data.quantity ?? 1)) || + (data.type === 'Raids' && + (data as RaidsOptions).isFakeMass && + ((data as RaidsOptions).maxSizeInput ?? 0) >= 3 && + data.duration < Time.Minute * 14.5 * (data.quantity ?? 1)) } }, { @@ -92,11 +112,13 @@ export const grandmasterCombatAchievements: CombatAchievement[] = [ type: 'speed', monster: 'Chambers of Xeric: Challenge Mode', rng: { - chancePerKill: 30, + chancePerKill: 1, hasChance: data => data.type === 'Raids' && (data as RaidsOptions).challengeMode && - (data as RaidsOptions).users.length === 1 + (data as RaidsOptions).users.length === 1 && + !(data as RaidsOptions).isFakeMass && + data.duration < Time.Minute * 38.5 * (data.quantity ?? 1) } }, { @@ -118,8 +140,18 @@ export const grandmasterCombatAchievements: CombatAchievement[] = [ type: 'speed', monster: 'Chambers of Xeric: Challenge Mode', rng: { - chancePerKill: 30, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).challengeMode + chancePerKill: 1, + hasChance: data => + (data.type === 'Raids' && + (data as RaidsOptions).challengeMode && + (data as RaidsOptions).users.length >= 3 && + !(data as RaidsOptions).isFakeMass && + data.duration < Time.Minute * 27 * (data.quantity ?? 1)) || + (data.type === 'Raids' && + (data as RaidsOptions).challengeMode && + (data as RaidsOptions).isFakeMass && + ((data as RaidsOptions).maxSizeInput ?? 0) >= 3 && + data.duration < Time.Minute * 27 * (data.quantity ?? 1)) } }, { @@ -129,8 +161,18 @@ export const grandmasterCombatAchievements: CombatAchievement[] = [ type: 'speed', monster: 'Chambers of Xeric: Challenge Mode', rng: { - chancePerKill: 30, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).challengeMode + chancePerKill: 1, + hasChance: data => + (data.type === 'Raids' && + (data as RaidsOptions).challengeMode && + (data as RaidsOptions).users.length >= 5 && + !(data as RaidsOptions).isFakeMass && + data.duration < Time.Minute * 25 * (data.quantity ?? 1)) || + (data.type === 'Raids' && + (data as RaidsOptions).challengeMode && + (data as RaidsOptions).isFakeMass && + ((data as RaidsOptions).maxSizeInput ?? 0) >= 5 && + data.duration < Time.Minute * 25 * (data.quantity ?? 1)) } }, { diff --git a/src/lib/combat_achievements/master.ts b/src/lib/combat_achievements/master.ts index 99097525d2..14a54a0d9c 100644 --- a/src/lib/combat_achievements/master.ts +++ b/src/lib/combat_achievements/master.ts @@ -150,7 +150,8 @@ export const masterCombatAchievements: CombatAchievement[] = [ monster: 'Chambers of Xeric', rng: { chancePerKill: 44, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).users.length === 1 + hasChance: data => + data.type === 'Raids' && (data as RaidsOptions).users.length === 1 && !(data as RaidsOptions).isFakeMass } }, { @@ -161,7 +162,8 @@ export const masterCombatAchievements: CombatAchievement[] = [ monster: 'Chambers of Xeric', rng: { chancePerKill: 25, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).users.length === 1 + hasChance: data => + data.type === 'Raids' && (data as RaidsOptions).users.length === 1 && !(data as RaidsOptions).isFakeMass } }, { @@ -183,7 +185,8 @@ export const masterCombatAchievements: CombatAchievement[] = [ monster: 'Chambers of Xeric', rng: { chancePerKill: 22, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).users.length === 1 + hasChance: data => + data.type === 'Raids' && (data as RaidsOptions).users.length === 1 && !(data as RaidsOptions).isFakeMass } }, { @@ -261,7 +264,8 @@ export const masterCombatAchievements: CombatAchievement[] = [ monster: 'Chambers of Xeric', rng: { chancePerKill: 1, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).users.length === 1 + hasChance: data => + data.type === 'Raids' && (data as RaidsOptions).users.length === 1 && !(data as RaidsOptions).isFakeMass } }, { @@ -283,7 +287,8 @@ export const masterCombatAchievements: CombatAchievement[] = [ monster: 'Chambers of Xeric', rng: { chancePerKill: 33, - hasChance: data => data.type === 'Raids' && (data as RaidsOptions).users.length === 1 + hasChance: data => + data.type === 'Raids' && (data as RaidsOptions).users.length === 1 && !(data as RaidsOptions).isFakeMass } }, { @@ -308,7 +313,8 @@ export const masterCombatAchievements: CombatAchievement[] = [ hasChance: data => data.type === 'Raids' && (data as RaidsOptions).challengeMode && - (data as RaidsOptions).users.length === 1 + (data as RaidsOptions).users.length === 1 && + !(data as RaidsOptions).isFakeMass } }, { @@ -333,7 +339,8 @@ export const masterCombatAchievements: CombatAchievement[] = [ hasChance: data => data.type === 'Raids' && (data as RaidsOptions).challengeMode && - (data as RaidsOptions).users.length === 1 + (data as RaidsOptions).users.length === 1 && + !(data as RaidsOptions).isFakeMass } }, { diff --git a/src/lib/data/cox.ts b/src/lib/data/cox.ts index 480af94e32..9c52e3fdfd 100644 --- a/src/lib/data/cox.ts +++ b/src/lib/data/cox.ts @@ -18,9 +18,10 @@ import { getMinigameScore } from '../settings/minigames'; import { SkillsEnum } from '../skilling/types'; import { Gear, constructGearSetup } from '../structures/Gear'; import type { Skills } from '../types'; -import { randomVariation } from '../util'; +import { itemID, itemNameFromID, randomVariation, resolveItems } from '../util'; import getOSItem from '../util/getOSItem'; import { logError } from '../util/logError'; +import { getSimilarItems } from './similarItems'; const bareMinStats: Skills = { attack: 80, @@ -31,10 +32,45 @@ const bareMinStats: Skills = { prayer: 70 }; -const SANGUINESTI_CHARGES_PER_COX = 150; -const SHADOW_CHARGES_PER_COX = 130; +export const coxUniques = [ + 'Dexterous prayer scroll', + 'Arcane prayer scroll', + 'Twisted buckler', + 'Dragon hunter crossbow', + "Dinh's bulwark", + 'Ancestral hat', + 'Ancestral robe top', + 'Ancestral robe bottom', + 'Dragon claws', + 'Elder maul', + 'Kodai insignia', + 'Twisted bow' +]; + +export const coxCMUniques = ['Metamorphic dust', 'Twisted ancestral colour kit']; + +const SCYTHE_CHARGERS_PER_COX = 100; +const SHADOW_CHARGES_PER_COX = 120; +const SANGUINESTI_CHARGES_PER_COX = 160; const TENTACLE_CHARGES_PER_COX = 200; +const REQUIRED_BOW = resolveItems(['Twisted bow', 'Bow of faerdhinen (c)', 'Magic shortbow']); +const REQUIRED_ARROWS = resolveItems(['Dragon arrow', 'Amethyst arrow', 'Rune arrow', 'Adamant arrow']); +const BOW_ARROWS_NEEDED = 150; +const REQUIRED_CROSSBOW = resolveItems([ + 'Zaryte crossbow', + 'Dragon hunter crossbow', + 'Armadyl crossbow', + 'rune crossbow' +]); +const REQUIRED_BOLTS = resolveItems([ + 'Ruby dragon bolts (e)', + 'Diamond dragon bolts (e)', + 'Dragon bolts', + 'Runite bolts' +]); +const CROSSBOW_BOLTS_NEEDED = 150; + export function hasMinRaidsRequirements(user: MUser) { return user.hasSkillReqs(bareMinStats); } @@ -159,7 +195,8 @@ function calcSetupPercent( return totalPercent; } -const maxMageGear = constructGearSetup({ +// BIS CoX gear: https://oldschool.runescape.wiki/w/Chambers_of_Xeric/Strategies#Melee +export const COXMaxMageGear = constructGearSetup({ head: 'Ancestral hat', neck: 'Occult necklace', body: 'Ancestral robe top', @@ -167,39 +204,37 @@ const maxMageGear = constructGearSetup({ hands: 'Tormented bracelet', legs: 'Ancestral robe bottom', feet: 'Eternal boots', - weapon: 'Harmonised nightmare staff', - shield: 'Arcane spirit shield', + '2h': "Tumeken's shadow", ring: 'Magus ring' }); -const maxMage = new Gear(maxMageGear); +const maxMage = new Gear(COXMaxMageGear); -const maxRangeGear = constructGearSetup({ - head: 'Armadyl helmet', +export const COXMaxRangeGear = constructGearSetup({ + head: 'Masori mask(f)', neck: 'Necklace of anguish', - body: 'Armadyl chestplate', - cape: "Ava's assembler", + body: 'Masori Body (f)', + cape: "Blessed dizana's quiver", hands: 'Zaryte vambraces', - legs: 'Armadyl chainskirt', + legs: 'Masori chaps (f)', feet: 'Pegasian boots', '2h': 'Twisted bow', ring: 'Venator ring', ammo: 'Dragon arrow' }); -const maxRange = new Gear(maxRangeGear); +const maxRange = new Gear(COXMaxRangeGear); -const maxMeleeGear = constructGearSetup({ - head: "Inquisitor's great helm", +export const COXMaxMeleeGear = constructGearSetup({ + head: 'Torva full helm', neck: 'Amulet of torture', - body: "Inquisitor's hauberk", + body: 'Torva platebody', cape: 'Infernal cape', hands: 'Ferocious gloves', - legs: "Inquisitor's plateskirt", + legs: 'Torva platelegs', feet: 'Primordial boots', - weapon: "Inquisitor's mace", - shield: 'Avernic defender', + '2h': 'Scythe of vitur', ring: 'Ultor ring' }); -const maxMelee = new Gear(maxMeleeGear); +const maxMelee = new Gear(COXMaxMeleeGear); export function calculateUserGearPercents(user: MUser) { const melee = calcSetupPercent( @@ -282,10 +317,42 @@ export async function checkCoxTeam(users: MUser[], cm: boolean, quantity = 1): P if (user.minionIsBusy) { return `${user.usernameOrMention}'s minion is already doing an activity and cannot join.`; } + + // Range weapon/ammo check + const rangeAmmo = user.gear.range.ammo; + const rangeWeapon = user.gear.range.equippedWeapon(); + const arrowsNeeded = BOW_ARROWS_NEEDED * quantity; + const boltsNeeded = CROSSBOW_BOLTS_NEEDED * quantity; + if (!rangeWeapon) return `<@${user.id}> Where is your range weapon?`; + if (rangeWeapon?.id) { + if (rangeWeapon.id !== itemID('Bow of faerdhinen (c)')) { + if (REQUIRED_BOW.includes(rangeWeapon.id)) { + if (!rangeAmmo || rangeAmmo.quantity < arrowsNeeded || !REQUIRED_ARROWS.includes(rangeAmmo.item)) { + return `<@${user.id}> needs ${arrowsNeeded} of one of these arrows equipped: ${REQUIRED_ARROWS.map(itemNameFromID).join(', ')}.`; + } + } else if (REQUIRED_CROSSBOW.includes(rangeWeapon.id)) { + if (!rangeAmmo || rangeAmmo.quantity < boltsNeeded || !REQUIRED_BOLTS.includes(rangeAmmo.item)) { + return `<@${user.id}> needs ${boltsNeeded} of ones of these bolts equipped: ${REQUIRED_BOLTS.map(itemNameFromID).join(', ')}.`; + } + } + } + } + + // Charge weapons check + if (user.gear.melee.hasEquipped('Scythe of vitur')) { + const scytheResult = checkUserCanUseDegradeableItem({ + item: getOSItem('Scythe of vitur'), + chargesToDegrade: SCYTHE_CHARGERS_PER_COX * quantity, + user + }); + if (!scytheResult.hasEnough) { + return scytheResult.userMessage; + } + } if (user.gear.melee.hasEquipped('Abyssal tentacle')) { const tentacleResult = checkUserCanUseDegradeableItem({ item: getOSItem('Abyssal tentacle'), - chargesToDegrade: TENTACLE_CHARGES_PER_COX, + chargesToDegrade: TENTACLE_CHARGES_PER_COX * quantity, user }); if (!tentacleResult.hasEnough) { @@ -295,7 +362,7 @@ export async function checkCoxTeam(users: MUser[], cm: boolean, quantity = 1): P if (user.gear.mage.hasEquipped('Sanguinesti staff')) { const sangResult = checkUserCanUseDegradeableItem({ item: getOSItem('Sanguinesti staff'), - chargesToDegrade: SANGUINESTI_CHARGES_PER_COX, + chargesToDegrade: SANGUINESTI_CHARGES_PER_COX * quantity, user }); if (!sangResult.hasEnough) { @@ -305,7 +372,7 @@ export async function checkCoxTeam(users: MUser[], cm: boolean, quantity = 1): P if (user.gear.mage.hasEquipped("Tumeken's shadow")) { const shadowResult = checkUserCanUseDegradeableItem({ item: getOSItem("Tumeken's shadow"), - chargesToDegrade: SHADOW_CHARGES_PER_COX, + chargesToDegrade: SHADOW_CHARGES_PER_COX * quantity, user }); if (!shadowResult.hasEnough) { @@ -336,23 +403,17 @@ function calcPerc(perc: number, num: number) { function teamSizeBoostPercent(size: number) { switch (size) { case 1: - return -10; + return -6; case 2: - return 12; + return 7; case 3: return 13; case 4: return 18; case 5: return 23; - case 6: - return 26; - case 7: - return 29; - case 8: - return 33; default: - return 35; + return 23; } } @@ -360,95 +421,192 @@ interface ItemBoost { item: Item; boost: number; mustBeEquipped: boolean; - setup?: 'mage' | 'range'; + setup?: 'melee' | 'mage' | 'range'; mustBeCharged?: boolean; requiredCharges?: number; } -const itemBoosts: ItemBoost[][] = [ +export const itemBoosts: ItemBoost[][] = [ [ + // melee weapon boost { - item: getOSItem('Twisted bow'), + item: getOSItem('Scythe of vitur'), boost: 8, - mustBeEquipped: false + mustBeEquipped: true, + setup: 'melee', + mustBeCharged: true, + requiredCharges: SCYTHE_CHARGERS_PER_COX }, { - item: getOSItem('Bow of faerdhinen (c)'), - boost: 6, - mustBeEquipped: false + item: getOSItem('Dragon hunter lance'), + boost: 5, + mustBeEquipped: true, + setup: 'melee' }, { - item: getOSItem('Dragon hunter crossbow'), - boost: 5, - mustBeEquipped: false + item: getOSItem('Soulreaper axe'), + boost: 4, + mustBeEquipped: true, + setup: 'melee' + }, + { + item: getOSItem("Osmumten's fang"), + boost: 3, + mustBeEquipped: true, + setup: 'melee' + }, + { + item: getOSItem('Abyssal tentacle'), + boost: 2, + mustBeEquipped: true, + setup: 'melee', + mustBeCharged: true, + requiredCharges: TENTACLE_CHARGES_PER_COX } ], [ + // Range weapon boost { - item: getOSItem('Dragon warhammer'), - boost: 3, - mustBeEquipped: false + item: getOSItem('Twisted bow'), + boost: 8, + mustBeEquipped: true, + setup: 'range' }, { - item: getOSItem('Bandos godsword'), - boost: 2.5, - mustBeEquipped: false + item: getOSItem('Bow of faerdhinen (c)'), + boost: 5, + mustBeEquipped: true, + setup: 'range' }, { - item: getOSItem('Bandos godsword (or)'), - boost: 2.5, - mustBeEquipped: false + item: getOSItem('Dragon hunter crossbow'), + boost: 4, + mustBeEquipped: true, + setup: 'range' + }, + { + item: getOSItem('Zaryte crossbow'), + boost: 3, + mustBeEquipped: true, + setup: 'range' } ], [ + // range ammo boost { - item: getOSItem('Dragon hunter lance'), + item: getOSItem('Dragon arrow'), boost: 3, - mustBeEquipped: false + mustBeEquipped: true, + setup: 'range' }, { - item: getOSItem('Abyssal tentacle'), + item: getOSItem('Ruby dragon bolts (e)'), boost: 2, - mustBeEquipped: false, - mustBeCharged: true, - requiredCharges: TENTACLE_CHARGES_PER_COX + mustBeEquipped: true, + setup: 'range' + }, + { + item: getOSItem('Diamond dragon bolts (e)'), + boost: 2, + mustBeEquipped: true, + setup: 'range' + }, + { + item: getOSItem('Amethyst arrow'), + boost: 1, + mustBeEquipped: true, + setup: 'range' + }, + { + item: getOSItem('Ruby dragon bolts'), + boost: 1, + mustBeEquipped: true, + setup: 'range' + }, + { + item: getOSItem('Diamond dragon bolts'), + boost: 1, + mustBeEquipped: true, + setup: 'range' + }, + { + item: getOSItem('Dragon bolts'), + boost: 1, + mustBeEquipped: true, + setup: 'range' } ], [ + // mage weapon boost { item: getOSItem("Tumeken's shadow"), - boost: 9, - mustBeEquipped: false, + boost: 8, + mustBeEquipped: true, setup: 'mage', mustBeCharged: true, requiredCharges: SHADOW_CHARGES_PER_COX }, { item: getOSItem('Sanguinesti staff'), - boost: 6, - mustBeEquipped: false, + boost: 4, + mustBeEquipped: true, setup: 'mage', mustBeCharged: true, requiredCharges: SANGUINESTI_CHARGES_PER_COX } ], [ + // defense reduction weapon boost { - item: getOSItem('Zaryte vambraces'), - boost: 4, - mustBeEquipped: true, - setup: 'range' + item: getOSItem('Elder maul'), + boost: 5, + mustBeEquipped: false + }, + { + item: getOSItem('Dragon warhammer'), + boost: 3, + mustBeEquipped: false + }, + { + item: getOSItem('Bandos godsword'), + boost: 2.5, + mustBeEquipped: false + } + ], + [ + // zaryte crossbow spec weapon + { + item: getOSItem('Zaryte crossbow'), + boost: 3, + mustBeEquipped: false + } + ], + [ + // lightbearer increases spec + { + item: getOSItem('Lightbearer'), + boost: 2, + mustBeEquipped: false + } + ], + [ + // pickaxe boost + { + item: getOSItem('Dragon pickaxe'), + boost: 1, + mustBeEquipped: false } ] ]; -const speedReductionForGear = 16; -const speedReductionForKC = 40; +const speedReductionForGear = 14; +const speedReductionForKC = 28; -const maxSpeedReductionFromItems = itemBoosts.reduce( +export const maxSpeedReductionFromItems = itemBoosts.reduce( (sum, items) => sum + Math.max(...items.map(item => item.boost)), 0 ); + const maxSpeedReductionUser = speedReductionForGear + speedReductionForKC + maxSpeedReductionFromItems; const baseDuration = Time.Minute * 83; @@ -467,10 +625,11 @@ export async function calcCoxDuration( const size = team.length; let totalReduction = 0; - + // console.log(`maxSpeedReductionFromItems: ${maxSpeedReductionFromItems}`); + // console.log(`maxSpeedReductionUser: ${maxSpeedReductionUser}`); const reductions: Record = {}; - // Track degradeable items: + // Track degradeable items (fakemass works properly with this code, it wont remove 5x charges): const degradeableItems: { item: Item; user: MUser; chargesToDegrade: number }[] = []; for (const u of team) { @@ -479,6 +638,7 @@ export async function calcCoxDuration( // Reduce time for gear const { total } = calculateUserGearPercents(u); userPercentChange += calcPerc(total, speedReductionForGear); + // console.log(`userPercentChange: ${userPercentChange}`); // Reduce time for KC const stats = await u.fetchMinigames(); @@ -486,10 +646,25 @@ export async function calcCoxDuration( userPercentChange += calcPerc(kcPercent, speedReductionForKC); // Reduce time for item boosts - itemBoosts.forEach(set => { + for (const set of itemBoosts) { for (const item of set) { - if (item.mustBeCharged && item.requiredCharges) { - if (u.hasEquippedOrInBank(item.item.id)) { + const simItems = getSimilarItems(item.item.id); + if (item.mustBeEquipped && item.mustBeCharged && item.requiredCharges) { + if (u.hasEquipped(simItems)) { + const testItem = { + item: item.item, + user: u, + chargesToDegrade: item.requiredCharges + }; + const canDegrade = checkUserCanUseDegradeableItem(testItem); + if (canDegrade.hasEnough) { + userPercentChange += item.boost; + degradeableItems.push(testItem); + break; + } + } + } else if (item.mustBeCharged && item.requiredCharges) { + if (u.hasEquippedOrInBank(simItems)) { const testItem = { item: item.item, user: u, @@ -503,28 +678,30 @@ export async function calcCoxDuration( } } } else if (item.mustBeEquipped) { - if (item.setup && u.gear[item.setup].hasEquipped(item.item.id)) { + if (item.setup && u.gear[item.setup].hasEquipped(simItems)) { userPercentChange += item.boost; break; - } else if (!item.setup && u.hasEquipped(item.item.id)) { + } else if (!item.setup && u.hasEquipped(simItems)) { userPercentChange += item.boost; break; } - } else if (u.hasEquippedOrInBank(item.item.id)) { + } else if (u.hasEquippedOrInBank(simItems)) { userPercentChange += item.boost; break; } } - }); + } + + const perc = Math.min(100, userPercentChange / size); - totalReduction += userPercentChange / size; - reductions[u.id] = userPercentChange / size; + totalReduction += perc; + reductions[u.id] = perc; } let duration = baseDuration; if (challengeMode) { duration = baseCmDuration; - duration = reduceNumByPercent(duration, totalReduction / 1.3); + duration = reduceNumByPercent(duration, totalReduction / 1.05); } else { duration = reduceNumByPercent(duration, totalReduction); } @@ -534,17 +711,30 @@ export async function calcCoxDuration( return { duration, reductions, maxUserReduction: maxSpeedReductionUser / size, degradeables: degradeableItems }; } -export async function calcCoxInput(u: MUser, solo: boolean) { - const items = new Bank(); - const kc = await getMinigameScore(u.id, 'raids'); - items.add('Stamina potion(4)', solo ? 2 : 1); - - let brewsNeeded = Math.max(1, 8 - Math.max(1, Math.ceil((kc + 1) / 30))); - if (solo) brewsNeeded++; - const restoresNeeded = Math.max(1, Math.floor(brewsNeeded / 3)); - - items.add('Saradomin brew(4)', brewsNeeded); - items.add('Super restore(4)', restoresNeeded); - - return items; +export async function calcCoxInput(u: MUser, quantity: number, solo: boolean) { + const supplies = new Bank(); + const ammo = new Bank(); + for (let i = 0; i < quantity; i++) { + const kc = await getMinigameScore(u.id, 'raids'); + supplies.add('Stamina potion(4)', solo ? 2 : 1); + + let brewsNeeded = Math.max(1, 8 - Math.max(1, Math.ceil((kc + 1) / 30))); + if (solo) brewsNeeded++; + const restoresNeeded = Math.max(1, Math.floor(brewsNeeded / 3)); + + supplies.add('Saradomin brew(4)', brewsNeeded); + supplies.add('Super restore(4)', restoresNeeded); + + // get ammo usage (checkCoxTeam() handles checking the proper amount and correct ammo type) + const rangeAmmo = u.gear.range.ammo; + const rangeWeapon = u.gear.range.equippedWeapon(); + if (rangeWeapon?.id !== itemID('Bow of faerdhinen (c)') && rangeWeapon && rangeAmmo) { + if (REQUIRED_BOW.includes(rangeWeapon.id)) { + ammo.add(rangeAmmo.item, BOW_ARROWS_NEEDED); + } else if (REQUIRED_CROSSBOW.includes(rangeWeapon.id)) { + ammo.add(rangeAmmo.item, CROSSBOW_BOLTS_NEEDED); + } + } + } + return { supplies, ammo }; } diff --git a/src/lib/data/similarItems.ts b/src/lib/data/similarItems.ts index 617ba01e48..33823e7413 100644 --- a/src/lib/data/similarItems.ts +++ b/src/lib/data/similarItems.ts @@ -114,7 +114,7 @@ const source: [string, (string | number)[]][] = [ ['Occult necklace', ['Occult necklace (or)']], ['Dragon hunter crossbow', ['Dragon hunter crossbow (t)', 'Dragon hunter crossbow (b)']], ['Armadyl crossbow', ['Zaryte crossbow']], - ['Dragon pickaxe', ['Dragon pickaxe(or)', 12_797, '3rd age pickaxe', 'Infernal pickaxe']], + ['Dragon pickaxe', ['Dragon pickaxe(or)', 12_797, 'Crystal pickaxe', '3rd age pickaxe', 'Infernal pickaxe']], ['Steam battlestaff', [12_795]], ['Lava battlestaff', [21_198]], ['Odium ward', [12_807]], diff --git a/src/lib/party.ts b/src/lib/party.ts index f1736a5974..2cd38a68e9 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -46,7 +46,7 @@ export async function setupParty(channel: TextChannel, leaderUser: MUser, option await Promise.all(usersWhoConfirmed.map(u => getUsername(u))) ).join( ', ' - )}\n\nThis party will automatically depart in 2 minutes, or if the leader clicks the start (start early) or stop button.`, + )}\n\nThis party will automatically depart in 5 minutes, or if the leader clicks the start (start early) or stop button.`, components: makeComponents(buttons.map(i => i.button)), allowedMentions: { users: [] diff --git a/src/lib/types/minions.ts b/src/lib/types/minions.ts index 36f325449c..fe2b654b72 100644 --- a/src/lib/types/minions.ts +++ b/src/lib/types/minions.ts @@ -439,6 +439,8 @@ export interface RaidsOptions extends ActivityTaskOptionsWithUsers { leader: string; users: string[]; challengeMode: boolean; + isFakeMass: boolean; + maxSizeInput?: number; quantity?: number; } diff --git a/src/lib/util/repeatStoredTrip.ts b/src/lib/util/repeatStoredTrip.ts index b156d56b8b..60fd11461b 100644 --- a/src/lib/util/repeatStoredTrip.ts +++ b/src/lib/util/repeatStoredTrip.ts @@ -488,15 +488,18 @@ const tripHandlers = { }, [activity_type_enum.Raids]: { commandName: 'raid', - args: (data: RaidsOptions) => ({ - cox: { - start: { - challenge_mode: data.challengeMode, - type: data.users.length === 1 ? 'solo' : 'mass', - quantity: data.quantity + args: (data: RaidsOptions) => { + return { + cox: { + start: { + challenge_mode: data.challengeMode, + type: data.isFakeMass ? 'fakemass' : data.users.length === 1 ? 'solo' : 'mass', + max_team_size: data.maxSizeInput, + quantity: data.quantity + } } - } - }) + }; + } }, [activity_type_enum.RoguesDenMaze]: { commandName: 'minigames', diff --git a/src/mahoji/commands/raid.ts b/src/mahoji/commands/raid.ts index aa803b78a3..79c0e0f981 100644 --- a/src/mahoji/commands/raid.ts +++ b/src/mahoji/commands/raid.ts @@ -5,7 +5,7 @@ import type { RaidLevel } from '../../lib/simulation/toa'; import { mileStoneBaseDeathChances, toaHelpCommand, toaStartCommand } from '../../lib/simulation/toa'; import { deferInteraction } from '../../lib/util/interactionReply'; import { minionIsBusy } from '../../lib/util/minionIsBusy'; -import { coxCommand, coxStatsCommand } from '../lib/abstracted_commands/coxCommand'; +import { coxBoostsCommand, coxCommand, coxStatsCommand } from '../lib/abstracted_commands/coxCommand'; import { tobCheckCommand, tobStartCommand, tobStatsCommand } from '../lib/abstracted_commands/tobCommand'; import type { OSBMahojiCommand } from '../lib/util'; @@ -29,8 +29,8 @@ export const raidCommand: OSBMahojiCommand = { { type: ApplicationCommandOptionType.String, name: 'type', - description: 'Choose whether you want to solo or mass.', - choices: ['solo', 'mass'].map(i => ({ name: i, value: i })), + description: 'Choose whether you want to solo, mass, or fake mass.', + choices: ['solo', 'mass', 'fakemass'].map(i => ({ name: i, value: i })), required: true }, { @@ -59,6 +59,11 @@ export const raidCommand: OSBMahojiCommand = { type: ApplicationCommandOptionType.Subcommand, name: 'stats', description: 'Check your CoX stats.' + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'itemboosts', + description: 'Check your CoX item boosts.' } ] }, @@ -179,8 +184,14 @@ export const raidCommand: OSBMahojiCommand = { channelID }: CommandRunOptions<{ cox?: { - start?: { type: 'solo' | 'mass'; challenge_mode?: boolean; max_team_size?: number; quantity?: number }; + start?: { + type: 'solo' | 'mass' | 'fakemass'; + challenge_mode?: boolean; + max_team_size?: number; + quantity?: number; + }; stats?: {}; + itemboosts?: {}; }; tob?: { start?: { solo?: boolean; hard_mode?: boolean; max_team_size?: number; quantity?: number }; @@ -196,6 +207,7 @@ export const raidCommand: OSBMahojiCommand = { const user = await mUserFetch(userID); const { cox, tob } = options; if (cox?.stats) return coxStatsCommand(user); + if (cox?.itemboosts) return coxBoostsCommand(user); if (tob?.stats) return tobStatsCommand(user); if (tob?.check) return tobCheckCommand(user, Boolean(tob.check.hard_mode)); if (options.toa?.help) return toaHelpCommand(user, channelID); diff --git a/src/mahoji/commands/testpotato.ts b/src/mahoji/commands/testpotato.ts index d429bc1bdb..bff36d218f 100644 --- a/src/mahoji/commands/testpotato.ts +++ b/src/mahoji/commands/testpotato.ts @@ -8,7 +8,7 @@ import { Time, noOp } from 'e'; import { Bank, Items } from 'oldschooljs'; import { convertLVLtoXP, itemID, toKMB } from 'oldschooljs/dist/util'; -import { resolveItems } from 'oldschooljs/dist/util/util'; +import { getItem, resolveItems } from 'oldschooljs/dist/util/util'; import { production } from '../../config'; import { mahojiUserSettingsUpdate } from '../../lib/MUser'; import { allStashUnitTiers, allStashUnitsFlat } from '../../lib/clues/stashUnits'; @@ -23,6 +23,7 @@ import { MAX_QP } from '../../lib/minions/data/quests'; import { allOpenables } from '../../lib/openables'; import { Minigames } from '../../lib/settings/minigames'; +import { COXMaxMageGear, COXMaxMeleeGear, COXMaxRangeGear } from '../../lib/data/cox'; import { getFarmingInfo } from '../../lib/skilling/functions/getFarmingInfo'; import Skills from '../../lib/skilling/skills'; import Farming from '../../lib/skilling/skills/farming'; @@ -38,6 +39,7 @@ import getOSItem from '../../lib/util/getOSItem'; import { logError } from '../../lib/util/logError'; import { parseStringBank } from '../../lib/util/parseStringBank'; import { userEventToStr } from '../../lib/util/userEvents'; +import { gearViewCommand } from '../lib/abstracted_commands/gearCommands'; import { getPOH } from '../lib/abstracted_commands/pohCommand'; import { allUsableItems } from '../lib/abstracted_commands/useCommand'; import { BingoManager } from '../lib/bingo/BingoManager'; @@ -92,6 +94,12 @@ for (const gear of resolveItems([ } const gearPresets = [ + { + name: 'Cox', + melee: COXMaxMeleeGear, + mage: COXMaxMageGear, + range: COXMaxRangeGear + }, { name: 'ToB', melee: TOBMaxMeleeGear, @@ -824,12 +832,21 @@ ${droprates.join('\n')}`), } if (options.gear) { const gear = gearPresets.find(i => stringMatches(i.name, options.gear?.thing))!; + + for (const type of ['melee', 'range', 'mage'] as const) { + const currentGear = gear[type]; + if (currentGear.ammo && getItem(currentGear.ammo.item)?.stackable) { + currentGear.ammo.quantity = 10000; + } + } + await user.update({ gear_melee: gear.melee.raw() as any, gear_range: gear.range.raw() as any, gear_mage: gear.mage.raw() as any }); - return `Set your gear for ${gear.name}.`; + + return gearViewCommand(user, 'all', false); } if (options.reset) { const resettable = thingsToReset.find(i => i.name === options.reset?.thing); diff --git a/src/mahoji/lib/abstracted_commands/coxCommand.ts b/src/mahoji/lib/abstracted_commands/coxCommand.ts index 6eeaca3135..85f5ba450b 100644 --- a/src/mahoji/lib/abstracted_commands/coxCommand.ts +++ b/src/mahoji/lib/abstracted_commands/coxCommand.ts @@ -7,10 +7,14 @@ import { calcCoxInput, calculateUserGearPercents, checkCoxTeam, + coxUniques, createTeam, hasMinRaidsRequirements, + itemBoosts, + maxSpeedReductionFromItems, minimumCoxSuppliesNeeded } from '../../../lib/data/cox'; +import { getSimilarItems } from '../../../lib/data/similarItems'; import { degradeItem } from '../../../lib/degradeableItems'; import { trackLoot } from '../../../lib/lootTrack'; import { setupParty } from '../../../lib/party'; @@ -23,20 +27,68 @@ import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; import { updateBankSetting } from '../../../lib/util/updateBankSetting'; import { mahojiParseNumber } from '../../mahojiSettings'; -const uniques = [ - 'Dexterous prayer scroll', - 'Arcane prayer scroll', - 'Twisted buckler', - 'Dragon hunter crossbow', - "Dinh's bulwark", - 'Ancestral hat', - 'Ancestral robe top', - 'Ancestral robe bottom', - 'Dragon claws', - 'Elder maul', - 'Kodai insignia', - 'Twisted bow' -]; +export async function coxBoostsCommand(user: MUser) { + const boostStr = []; + let workFromBank = false; + let boostPercent = 0; + boostStr.push('<:Twisted_bow:403018312402862081> Chambers of Xeric <:Olmlet:324127376873357316>\n'); + boostStr.push( + '*Item boosts help reduce the time required to complete Chambers. Only one boost from each bullet point can be applied. The further left the higher the boost.*\n\n' + ); + boostStr.push('**Equipped boost Items:**\n'); + for (const set of itemBoosts) { + if (set.some(item => !item.mustBeEquipped) && workFromBank === false) { + boostStr.push('**Works from bank:**\n'); + workFromBank = true; + } + boostStr.push('- '); + const ownedItems = set.filter(item => { + if (item.mustBeEquipped) { + if (item.setup && user.gear[item.setup].hasEquipped(item.item.id, false, true)) { + return true; + } + } else { + return user.hasEquippedOrInBank(getSimilarItems(item.item.id)); + } + }); + if (ownedItems.length > 0) { + const maxBoost = Math.max(...ownedItems.map(item => item.boost)); + const setItems = set.map(item => { + if (item.item.name === 'Dragon pickaxe') { + if (item.boost === maxBoost && ownedItems.some(ownedItem => ownedItem.item.id === item.item.id)) { + boostPercent += item.boost; + return `${Emoji.Tick}Pickaxe Boost (3a, Crystal, Dragon)`; + } else { + return `${Emoji.RedX}Pickaxe Boost (3a, Crystal, Dragon)`; + } + } else { + if (item.boost === maxBoost && ownedItems.some(ownedItem => ownedItem.item.id === item.item.id)) { + boostPercent += item.boost; + return `${Emoji.Tick}${item.item.name}`; + } else { + return `${Emoji.RedX}${item.item.name}`; + } + } + }); + boostStr.push(setItems.join(', ')); + } else { + const setItems = set.map(item => { + if (item.item.name === 'Dragon pickaxe') { + return `${Emoji.RedX}Pickaxe Boost (3a, Crystal, Dragon)`; + } else { + return `${Emoji.RedX}${item.item.name}`; + } + }); + boostStr.push(setItems.join(', ')); + } + boostStr.push('\n'); + } + const finalPercentage = ((boostPercent / maxSpeedReductionFromItems) * 100).toFixed(1); + boostStr.push( + `\nYour CoX Item Boost is: **${finalPercentage === '100.0' ? '100' : finalPercentage}/100%**\nEffectively lowering your raid time by: **${boostPercent} minutes**` + ); + return boostStr.join(''); +} export async function coxStatsCommand(user: MUser) { const [minigameScores, stats] = await Promise.all([ @@ -45,7 +97,7 @@ export async function coxStatsCommand(user: MUser) { ]); let totalUniques = 0; const { cl } = user; - for (const item of uniques) { + for (const item of coxUniques) { totalUniques += cl.amount(item); } const totalPoints = stats.total_cox_points; @@ -75,19 +127,20 @@ export async function coxStatsCommand(user: MUser) { **Melee:** <:Elder_maul:403018312247803906> ${melee.toFixed(1)}% **Range:** <:Twisted_bow:403018312402862081> ${range.toFixed(1)}% **Mage:** <:Kodai_insignia:403018312264712193> ${mage.toFixed(1)}% -**Total Gear Score:** ${Emoji.Gear} ${total.toFixed(1)}%`; +**Total Gear Score:** ${Emoji.Gear} ${total.toFixed(1)}%\n +Check \`/raid cox itemboosts\` for more information on Item boosts.`; } export async function coxCommand( channelID: string, user: MUser, - type: 'solo' | 'mass', + type: 'solo' | 'mass' | 'fakemass', maxSizeInput: number | undefined, isChallengeMode: boolean, _quantity?: number ) { - if (type !== 'mass' && type !== 'solo') { - return 'Specify your team setup for Chambers of Xeric, either solo or mass.'; + if (type !== 'mass' && type !== 'solo' && type !== 'fakemass') { + return 'Specify your team setup for Chambers of Xeric, either solo, mass, or mass (4 bots teammates).'; } const minigameID = isChallengeMode ? 'raids_challenge_mode' : 'raids'; @@ -154,7 +207,10 @@ export async function coxCommand( if (!channelIsSendable(channel)) return 'No channel found.'; let users: MUser[] = []; - if (type === 'mass') { + const fakeUsers = Math.min(maxSizeInput ?? 5, maxSize); + if (type === 'fakemass') { + users = new Array(fakeUsers).fill(user); + } else if (type === 'mass') { users = (await setupParty(channel, user, partyOptions)).filter(u => !u.minionIsBusy); } else { users = [user]; @@ -175,16 +231,20 @@ export async function coxCommand( return `Your mass failed to start because of this reason: ${teamCheckFailure}`; } - // This gives a normal duration distribution. Better than (raidDuration * quantity) +/- 5% + // add variance to cox raid time const duration = sumArr( Array(quantity) .fill(raidDuration) .map(d => randomVariation(d, 5)) ); + let debugStr = ''; const isSolo = users.length === 1; + const isFakeMass = users.length > 1 && new Set(users).size === 1; - const totalCost = new Bank(); + for (const d of degradeables) { + d.chargesToDegrade *= quantity; + } await Promise.all( degradeables.map(async d => { @@ -192,11 +252,17 @@ export async function coxCommand( }) ); + const totalCost = new Bank(); + const usersToCheck = isFakeMass ? [users[0]] : users; + const costResult = await Promise.all([ - ...users.map(async u => { - const supplies = await calcCoxInput(u, isSolo); + ...usersToCheck.map(async u => { + const { supplies, ammo } = await calcCoxInput(u, quantity, isSolo); await u.removeItemsFromBank(supplies); totalCost.add(supplies); + const realAmmoCost = await u.specialRemoveItems(ammo); + totalCost.add(realAmmoCost.realCost); + supplies.add(realAmmoCost.realCost); const { total } = calculateUserGearPercents(u); debugStr += `${u.usernameOrMention} (${Emoji.Gear}${total.toFixed(1)}% ${ Emoji.CombatSword @@ -227,8 +293,10 @@ export async function coxCommand( duration, type: 'Raids', leader: user.id, - users: users.map(u => u.id), + users: usersToCheck.map(u => u.id), challengeMode: isChallengeMode, + maxSizeInput: isFakeMass ? fakeUsers : maxSize, + isFakeMass, quantity }); @@ -236,11 +304,15 @@ export async function coxCommand( ? `${user.minionName} is now doing ${quantity > 1 ? quantity : 'a'} Chambers of Xeric raid${ quantity > 1 ? 's' : '' }. The total trip will take ${formatDuration(duration)}.` - : `${partyOptions.leader.usernameOrMention}'s party (${users - .map(u => u.usernameOrMention) - .join(', ')}) is now off to do ${quantity > 1 ? quantity : 'a'} Chambers of Xeric raid${ - quantity > 1 ? 's' : '' - } - the total trip will take ${formatDuration(duration)}.`; + : isFakeMass + ? `${partyOptions.leader.usernameOrMention} your party of (${user.minionName} & ${users.length - 1} simulated users) is now off to do ${quantity > 1 ? quantity : 'a'} Chambers of Xeric raid${ + quantity > 1 ? 's' : '' + } - the total trip will take ${formatDuration(duration)}.` + : `${partyOptions.leader.usernameOrMention}'s party (${users + .map(u => u.usernameOrMention) + .join(', ')}) is now off to do ${quantity > 1 ? quantity : 'a'} Chambers of Xeric raid${ + quantity > 1 ? 's' : '' + } - the total trip will take ${formatDuration(duration)}.`; str += ` \n\n${debugStr}`; diff --git a/src/tasks/minions/minigames/raidsActivity.ts b/src/tasks/minions/minigames/raidsActivity.ts index 0ad3162d24..0cbc846bc4 100644 --- a/src/tasks/minions/minigames/raidsActivity.ts +++ b/src/tasks/minions/minigames/raidsActivity.ts @@ -7,7 +7,7 @@ import { ChambersOfXeric } from 'oldschooljs/dist/simulation/misc/ChambersOfXeri import { drawChestLootImage } from '../../../lib/bankImage'; import { Emoji, Events } from '../../../lib/constants'; import { chambersOfXericCL, chambersOfXericMetamorphPets } from '../../../lib/data/CollectionsExport'; -import { createTeam } from '../../../lib/data/cox'; +import { coxCMUniques, coxUniques, createTeam } from '../../../lib/data/cox'; import { trackLoot } from '../../../lib/lootTrack'; import { resolveAttackStyles } from '../../../lib/minions/functions'; import { getMinigameScore, incrementMinigameScore } from '../../../lib/settings/settings'; @@ -79,12 +79,23 @@ async function handleCoxXP(user: MUser, qty: number, isCm: boolean) { export const raidsTask: MinionTask = { type: 'Raids', async run(data: RaidsOptions) { - const { channelID, users, challengeMode, duration, leader, quantity: _quantity } = data; + const { + channelID, + users, + challengeMode, + isFakeMass, + maxSizeInput, + duration, + leader, + quantity: _quantity + } = data; const quantity = _quantity ?? 1; - const allUsers = await Promise.all(users.map(async u => mUserFetch(u))); + const fetchedUsers = await Promise.all(users.map(async u => mUserFetch(u))); + let allUsers = isFakeMass ? Array(maxSizeInput).fill(fetchedUsers[0]) : fetchedUsers; const previousCLs = allUsers.map(i => i.cl.clone()); let totalPoints = 0; + const fakeUserResults = new Map(); const raidResults = new Map(); for (let x = 0; x < quantity; x++) { const team = await createTeam(allUsers, challengeMode); @@ -94,14 +105,31 @@ export const raidsTask: MinionTask = { teamMate.canReceiveAncientTablet = false; } } - // Vary completion times for CM time limits - const timeToComplete = quantity === 1 ? duration : randomVariation(duration / quantity, 5); + + if (isFakeMass) { + // Remove the users ID from fake users + for (let i = 1; i < team.length; i++) { + team[i].id = `${i}`; + } + } + + // Vary completion times for multiple raids in 1 trip + const timeToComplete = quantity === 1 ? duration : randomVariation(duration / quantity, 2); const raidLoot = ChambersOfXeric.complete({ challengeMode, timeToComplete, team }); + for (const [userID, userLoot] of Object.entries(raidLoot)) { + //track the simulated users loot to show the user on trip return + if (isFakeMass) { + if (userID !== leader) { + const existingLoot = fakeUserResults.get(userID) || new Bank(); + existingLoot.add(userLoot); + fakeUserResults.set(userID, existingLoot); + } + } let userData = raidResults.get(userID); // Do all the one-time / per-user stuff: if (!userData) { @@ -122,14 +150,25 @@ export const raidsTask: MinionTask = { userData.deathChance = member.deathChance; totalPoints += member.personalPoints; - const hasDust = userData.loot.has('Metamorphic dust') || userData.mUser.cl.has('Metamorphic dust'); - if (challengeMode && roll(50) && hasDust) { - const result = userData.loot.clone().add(userData.mUser.allItemsOwned); - const unownedPet = shuffleArr(chambersOfXericMetamorphPets).find(pet => !result.has(pet)); - if (unownedPet) { - userLoot.add(unownedPet); + // logic for cox metamorph pets + const addMetamorphPet = (userData: RaidResultUser, userLoot: Bank, challengeMode: boolean) => { + const hasDust = userData.loot.has('Metamorphic dust') || userData.mUser.cl.has('Metamorphic dust'); + if (challengeMode && roll(50) && hasDust) { + const result = userData.loot.clone().add(userData.mUser.allItemsOwned); + const unownedPet = shuffleArr(chambersOfXericMetamorphPets).find(pet => !result.has(pet)); + if (unownedPet) { + userLoot.add(unownedPet); + } + } + }; + if (isFakeMass) { + if (userID === leader) { + addMetamorphPet(userData, userLoot, challengeMode); } + } else { + addMetamorphPet(userData, userLoot, challengeMode); } + if (userLoot.has('Ancient tablet')) { userData.gotAncientTablet = true; } @@ -142,10 +181,38 @@ export const raidsTask: MinionTask = { const minigameID = challengeMode ? 'raids_challenge_mode' : 'raids'; const totalLoot = new Bank(); - let resultMessage = `<@${leader}> Your ${challengeMode ? 'Challenge Mode Raid' : 'Raid'}${ quantity > 1 ? 's have' : ' has' - } finished. The total amount of points your team got is ${totalPoints.toLocaleString()}.\n`; + } finished. The total amount of points your team got is ${totalPoints.toLocaleString()}.`; + + // create the simulated users loot message + if (isFakeMass) { + const fakeUsersStr: string[] = []; + for (const [fakeID, fakeLoot] of fakeUserResults) { + const greenUnique = coxCMUniques.find(u => fakeLoot.has(u)); + const purpleUnique = coxUniques.find(u => fakeLoot.has(u)); + const fakeUserOlmlet = fakeLoot.has('Olmlet'); + fakeUsersStr.push( + `${fakeUserOlmlet ? '<:Olmlet:324127376873357316>' : ''}${greenUnique ? `${Emoji.Green}` : ''}${purpleUnique ? `${Emoji.Purple}` : ''}User #${fakeID}: ${fakeLoot}\n` + ); + } + const fakeUsersString = fakeUsersStr.join(' '); + const greenUnique = coxCMUniques.find(u => fakeUsersString.includes(u)); + const purpleUnique = coxUniques.find(u => fakeUsersString.includes(u)); + const fakeUserOlmlet = fakeUsersString.includes('Olmlet'); + resultMessage += `\n${`\n${fakeUserOlmlet ? '<:Olmlet:324127376873357316>' : ''}${greenUnique ? `${Emoji.Green}` : ''}${purpleUnique ? `${Emoji.Purple}` : ''}Simulated users loot:\n||${fakeUsersStr.join('')}||`}`; + } + + // Filter out fake users and only retain the leader's results + if (isFakeMass) { + allUsers = fetchedUsers; + const leaderResult = raidResults.get(leader) as RaidResultUser; + raidResults.clear(); + if (leaderResult) { + raidResults.set(leader, leaderResult); + } + } + await Promise.all(allUsers.map(u => incrementMinigameScore(u.id, minigameID, quantity))); for (const [userID, userData] of raidResults) { @@ -191,7 +258,7 @@ export const raidsTask: MinionTask = { const str = specialLoot ? `${emote} ||${itemsAdded}||` : itemsAdded.toString(); const deathStr = deaths === 0 ? '' : new Array(deaths).fill(Emoji.Skull).join(' '); - resultMessage += `\n${deathStr} **${user}** received: ${str} (${personalPoints?.toLocaleString()} pts, ${ + resultMessage += `\n${deathStr}${user} received: ${str} (${personalPoints?.toLocaleString()} pts, ${ Emoji.Skull }${deathChance.toFixed(0)}%) ${xpResult}`; }