diff --git a/package.json b/package.json index 6100e90895..a89ee4856a 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@types/jest-image-snapshot": "^6.1.0", "@types/lodash": "^4.14.195", "@types/madge": "^5.0.0", + "@types/mitm": "^1.3.8", "@types/node": "^14.18.12", "@types/node-cron": "^3.0.7", "@types/node-fetch": "^2.6.1", @@ -85,6 +86,7 @@ "eslint-plugin-unicorn": "^44.0.2", "fast-check": "^3.18.0", "jest-image-snapshot": "^6.2.0", + "mitm": "^1.7.2", "madge": "^7.0.0", "prettier": "^2.7.1", "prisma": "^5.13.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6f946311f6..c38c692aca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -984,10 +984,11 @@ model UserStats { high_gambles Int @default(0) honour_points Int @default(0) - slayer_task_streak Int @default(0) - slayer_superior_count Int @default(0) - slayer_unsired_offered Int @default(0) - slayer_chewed_offered Int @default(0) + slayer_task_streak Int @default(0) + slayer_wildy_task_streak Int @default(0) + slayer_superior_count Int @default(0) + slayer_unsired_offered Int @default(0) + slayer_chewed_offered Int @default(0) tob_cost Json @default("{}") tob_loot Json @default("{}") diff --git a/src/lib/MUser.ts b/src/lib/MUser.ts index dec37c2ed2..b0a9f20e51 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -901,20 +901,28 @@ Charge your items using ${mentionCommand(globalClient, 'minion', 'charge')}.` return this.caPoints() >= CombatAchievements[tier].rewardThreshold; } - buildCATertiaryItemChanges() { + buildTertiaryItemChanges(hasRingOfWealthI: boolean = false, inWildy: boolean = false, onTask: boolean = false) { const changes = new Map(); - if (this.hasCompletedCATier('easy')) { - changes.set('Clue scroll (easy)', 5); - } - if (this.hasCompletedCATier('medium')) { - changes.set('Clue scroll (medium)', 5); - } - if (this.hasCompletedCATier('hard')) { - changes.set('Clue scroll (hard)', 5); + + const tiers = Object.keys(CombatAchievements) as Array; + for (const tier of tiers) { + let change = hasRingOfWealthI ? 50 : 0; + if (this.hasCompletedCATier(tier)) { + change += 5; + } + changes.set(`Clue scroll (${tier})`, change); } - if (this.hasCompletedCATier('elite')) { - changes.set('Clue scroll (elite)', 5); + + if (inWildy) changes.set('Giant key', 50); + + if (inWildy && !onTask) { + changes.set('Mossy key', 60); + } else if (!inWildy && onTask) { + changes.set('Mossy key', 66.67); + } else if (inWildy && onTask) { + changes.set('Mossy key', 77.6); } + return changes; } diff --git a/src/lib/bankImage.ts b/src/lib/bankImage.ts index 20260b73bf..619a12825b 100644 --- a/src/lib/bankImage.ts +++ b/src/lib/bankImage.ts @@ -298,7 +298,7 @@ export const bankFlags = [ ] as const; export type BankFlag = (typeof bankFlags)[number]; -class BankImageTask { +export class BankImageTask { public itemIconsList: Set; public itemIconImagesCache: Map; public backgroundImages: BankBackground[] = []; @@ -855,6 +855,7 @@ class BankImageTask { if (!isTransparent && noBorder !== 1) { this.drawBorder(ctx, bgSprite, bgImage.name === 'Default'); } + await this.drawItems( ctx, compact, @@ -1045,5 +1046,6 @@ declare global { } } } -global.bankImageGenerator = new BankImageTask(); +export const bankImageTask = new BankImageTask(); +global.bankImageGenerator = bankImageTask; bankImageGenerator.init(); diff --git a/src/lib/bossEvents.ts b/src/lib/bossEvents.ts index bd2249642a..da4c8d74d8 100644 --- a/src/lib/bossEvents.ts +++ b/src/lib/bossEvents.ts @@ -12,7 +12,6 @@ import { import { Bank, LootTable } from 'oldschooljs'; import { OWNER_IDS, production } from '../config'; -import { scaryEatables } from './constants'; import { prisma } from './settings/prisma'; import { getPHeadDescriptor, @@ -26,6 +25,7 @@ import { BossInstance, BossOptions, BossUser } from './structures/Boss'; import { Gear } from './structures/Gear'; import { NewBossOptions } from './types/minions'; import { formatDuration, roll } from './util'; +import getOSItem from './util/getOSItem'; import { logError } from './util/logError'; import { sendToChannelID } from './util/webhook'; import { LampTable } from './xpLamps'; @@ -39,6 +39,45 @@ interface BossEvent { export const bossEventChannelID = production ? '897170239333220432' : '1023760501957722163'; +export const scaryEatables = [ + { + item: getOSItem('Candy teeth'), + healAmount: 3 + }, + { + item: getOSItem('Toffeet'), + healAmount: 5 + }, + { + item: getOSItem('Chocolified skull'), + healAmount: 8 + }, + { + item: getOSItem('Rotten sweets'), + healAmount: 9 + }, + { + item: getOSItem('Hairyfloss'), + healAmount: 12 + }, + { + item: getOSItem('Eyescream'), + healAmount: 13 + }, + { + item: getOSItem('Goblinfinger soup'), + healAmount: 20 + }, + { + item: getOSItem("Benny's brain brew"), + healAmount: 50 + }, + { + item: getOSItem('Roasted newt'), + healAmount: 120 + } +]; + function getScaryFoodFromBank(user: MUser, totalHealingNeeded: number, _userBank?: Bank): false | Bank { if (OWNER_IDS.includes(user.id)) return new Bank(); let totalHealingCalc = totalHealingNeeded; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index fbb7c5e3db..f3fde19940 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -668,45 +668,6 @@ export const mahojiInformationalButtons: APIButtonComponent[] = buttonSource.map export const PATRON_ONLY_GEAR_SETUP = 'Sorry - but the `other` gear setup is only available for Tier 3 Patrons (and higher) to use.'; -export const scaryEatables = [ - { - item: getOSItem('Candy teeth'), - healAmount: 3 - }, - { - item: getOSItem('Toffeet'), - healAmount: 5 - }, - { - item: getOSItem('Chocolified skull'), - healAmount: 8 - }, - { - item: getOSItem('Rotten sweets'), - healAmount: 9 - }, - { - item: getOSItem('Hairyfloss'), - healAmount: 12 - }, - { - item: getOSItem('Eyescream'), - healAmount: 13 - }, - { - item: getOSItem('Goblinfinger soup'), - healAmount: 20 - }, - { - item: getOSItem("Benny's brain brew"), - healAmount: 50 - }, - { - item: getOSItem('Roasted newt'), - healAmount: 120 - } -]; - export const projectiles = { arrow: { items: resolveItems(['Adamant arrow', 'Rune arrow', 'Amethyst arrow', 'Dragon arrow', 'Hellfire arrow']), @@ -878,7 +839,7 @@ const globalConfigSchema = z.object({ patreonCampaignID: z.coerce.number().int().default(1), patreonWebhookSecret: z.coerce.string().default(''), httpPort: z.coerce.number().int().default(8080), - clientID: z.string().min(15).max(25), + clientID: z.string().min(10).max(25), geAdminChannelID: z.string().default('') }); dotenv.config({ path: path.resolve(process.cwd(), process.env.TEST ? '.env.test' : '.env') }); diff --git a/src/lib/data/CollectionsExport.ts b/src/lib/data/CollectionsExport.ts index cb87c32cd0..7d616ecfd4 100644 --- a/src/lib/data/CollectionsExport.ts +++ b/src/lib/data/CollectionsExport.ts @@ -2020,9 +2020,9 @@ export const slayerCL = resolveItems([ 'Mystic gloves (dusk)', 'Mystic boots (dusk)', 'Basilisk jaw', - // "Dagon'hai hat", - // "Dagon'hai robe top", - // "Dagon'hai robe bottom", + "Dagon'hai hat", + "Dagon'hai robe top", + "Dagon'hai robe bottom", 'Blood shard', ...stoneSpirits.map(i => i.spirit.id), 'Brackish blade', diff --git a/src/lib/data/buyables/buyables.ts b/src/lib/data/buyables/buyables.ts index e426129749..e5ce2a4714 100644 --- a/src/lib/data/buyables/buyables.ts +++ b/src/lib/data/buyables/buyables.ts @@ -1142,6 +1142,15 @@ const Buyables: Buyable[] = [ return toaKCs.expertKC >= 25 ? [true] : [false, 'You need a 25 Expert KC in Tombs of Amascut to buy this.']; } }, + { + name: 'Lockpick', + gpCost: 5000, + ironmanPrice: 500, + skillsNeeded: { + agility: 50, + thieving: 50 + } + }, ...sepulchreBuyables, ...constructionBuyables, ...hunterBuyables, diff --git a/src/lib/data/creatablesTable.txt b/src/lib/data/creatablesTable.txt index d43d175b82..3e53924344 100644 --- a/src/lib/data/creatablesTable.txt +++ b/src/lib/data/creatablesTable.txt @@ -79,6 +79,7 @@ | Kodai wand | 1x Master wand, 1x Kodai insignia | 1x Kodai wand | 0 | | Salve amulet (e) | 1x Salve amulet | 1x Salve amulet (e) | 0 | | Salve amulet(ei) | 1x Salve amulet(i) | 1x Salve amulet(ei) | 0 | +| Ring of wealth (i) | 1x Ring of wealth, 1x Ring of wealth scroll | 1x Ring of wealth (i) | 50000 | | Strange hallowed tome | 1x Mysterious page 1, 1x Mysterious page 2, 1x Mysterious page 3, 1x Mysterious page 4, 1x Mysterious page 5 | 1x Strange hallowed tome | 0 | | Frozen key | 1x Frozen key piece (armadyl), 1x Frozen key piece (bandos), 1x Frozen key piece (zamorak), 1x Frozen key piece (saradomin) | 1x Frozen key | 0 | | Ecumenical key | 50x Ecumenical key shard | 1x Ecumenical key | 0 | diff --git a/src/lib/data/createables.ts b/src/lib/data/createables.ts index 34080000ce..9df35ea47d 100644 --- a/src/lib/data/createables.ts +++ b/src/lib/data/createables.ts @@ -2230,6 +2230,12 @@ const Createables: Createable[] = [ }, customReq: salveECustomReq }, + { + name: 'Ring of wealth (i)', + inputItems: new Bank().add('Ring of wealth').add('Ring of wealth scroll'), + GPCost: 50_000, + outputItems: new Bank().add('Ring of wealth (i)') + }, { name: 'Strange hallowed tome', inputItems: new Bank({ diff --git a/src/lib/geImage.ts b/src/lib/geImage.ts index 1e35e331ad..d66e6c3b9b 100644 --- a/src/lib/geImage.ts +++ b/src/lib/geImage.ts @@ -3,16 +3,11 @@ import { formatItemStackQuantity, generateHexColorForCashStack, toTitleCase } fr import { GEListing, GETransaction } from '@prisma/client'; import * as fs from 'fs/promises'; import { floor } from 'lodash'; -import fetch from 'node-fetch'; -import * as path from 'path'; import { GEListingWithTransactions } from './../mahoji/commands/ge'; import { GrandExchange } from './grandExchange'; import { fillTextXTimesInCtx } from './util/canvasUtil'; import getOSItem from './util/getOSItem'; -import { logError } from './util/logError'; - -const CACHE_DIR = './icon_cache'; function drawTitle(ctx: SKRSContext2D, title: string, canvas: Canvas) { // Draw Page Title @@ -34,20 +29,9 @@ class GeImageTask { public geProgressShadow: Image | null = null; public geIconBuy: Image | null = null; public geIconSell: Image | null = null; - public itemIconsList: Set; - public itemIconImagesCache: Map; - - public constructor() { - // This tells us simply whether the file exists or not on disk. - this.itemIconsList = new Set(); - - // If this file does exist, it might be cached in this, or need to be read from fs. - this.itemIconImagesCache = new Map(); - } async init() { await this.prepare(); - await this.run(); } async prepare() { @@ -74,23 +58,6 @@ class GeImageTask { ); } - async run() { - await this.cacheFiles(); - } - - async cacheFiles() { - // Ensure that the icon_cache dir exists. - await fs.mkdir(CACHE_DIR).catch(() => null); - CACHE_DIR; - // Get a list of all files (images) in the dir. - const filesInDir = await fs.readdir(CACHE_DIR); - - // For each one, set a cache value that it exists. - for (const fileName of filesInDir) { - this.itemIconsList.add(parseInt(path.parse(fileName).name)); - } - } - drawText( ctx: SKRSContext2D, text: string, @@ -148,7 +115,7 @@ class GeImageTask { if (listing) { // Get item - const itemImage = await this.getItemImage(listing.item_id); + const itemImage = await bankImageGenerator.getItemImage(listing.item_id); // Draw item ctx.textAlign = 'left'; @@ -221,40 +188,6 @@ class GeImageTask { } } - async getItemImage(itemID: number): Promise { - const cachedImage = this.itemIconImagesCache.get(itemID); - if (cachedImage) return cachedImage; - - const isOnDisk = this.itemIconsList.has(itemID); - if (!isOnDisk) { - await this.fetchAndCacheImage(itemID); - return this.getItemImage(itemID); - } - - const imageBuffer = await fs.readFile(path.join(CACHE_DIR, `${itemID}.png`)); - try { - const image = await loadImage(imageBuffer); - this.itemIconImagesCache.set(itemID, image); - return image; - } catch (err) { - logError(`Failed to load item icon with id: ${itemID}`); - return this.getItemImage(1); - } - } - - async fetchAndCacheImage(itemID: number) { - const imageBuffer = await fetch(`https://chisel.weirdgloop.org/static/img/osrs-sprite/${itemID}.png`).then( - result => result.buffer() - ); - - await fs.writeFile(path.join(CACHE_DIR, `${itemID}.png`), imageBuffer); - - const image = await loadImage(imageBuffer); - - this.itemIconsList.add(itemID); - this.itemIconImagesCache.set(itemID, image); - } - async createInterface(opts: { user: MUser; page: number; diff --git a/src/lib/minions/data/killableMonsters/bosses/wildy.ts b/src/lib/minions/data/killableMonsters/bosses/wildy.ts index 81529c8670..e4957cb3de 100644 --- a/src/lib/minions/data/killableMonsters/bosses/wildy.ts +++ b/src/lib/minions/data/killableMonsters/bosses/wildy.ts @@ -774,24 +774,33 @@ export const wildyKillableMonsters: KillableMonster[] = [ timeToFinish: Time.Minute * 4.3, emoji: '<:Pet_chaos_elemental:324127377070227456>', wildy: true, - + canBePked: true, + pkActivityRating: 4, + pkBaseDeathChance: 5, difficultyRating: 8, itemsRequired: deepResolveItems([ ['Pernix body', "Black d'hide body", "Karil's leathertop"], ['Pernix chaps', "Black d'hide chaps", "Karil's leatherskirt"] ]), qpRequired: 0, - itemInBankBoosts: [ + equippedItemBoosts: [ { - [itemID("Craw's bow")]: 20, - [itemID('Webweaver bow')]: 25 + items: [ + { boostPercent: 25, itemID: itemID('Webweaver bow') }, + { boostPercent: 20, itemID: itemID("Craw's bow") } + ], + gearSetup: 'wildy' }, { - [itemID('Archers ring')]: 3, - [itemID('Archers ring (i)')]: 5 + items: [ + { boostPercent: 5, itemID: itemID('Archers ring (i)') }, + { boostPercent: 3, itemID: itemID('Archers ring') } + ], + gearSetup: 'wildy' }, { - [itemID('Barrows gloves')]: 3 + items: [{ boostPercent: 3, itemID: itemID('Barrows gloves') }], + gearSetup: 'wildy' } ], defaultAttackStyles: [SkillsEnum.Attack], @@ -808,15 +817,27 @@ export const wildyKillableMonsters: KillableMonster[] = [ timeToFinish: Time.Minute * 3.3, emoji: '<:Ancient_staff:412845709453426689>', wildy: true, + canBePked: true, + pkActivityRating: 4, + pkBaseDeathChance: 2, difficultyRating: 6, qpRequired: 0, - itemInBankBoosts: [ + equippedItemBoosts: [ { - [itemID("Craw's bow")]: 20, - [itemID('Webweaver bow')]: 25 + items: [ + { boostPercent: 25, itemID: itemID('Webweaver bow') }, + { boostPercent: 20, itemID: itemID("Craw's bow") } + ], + gearSetup: 'wildy' }, - { [itemID("Karil's leathertop")]: 3 }, - { [itemID("Karil's leatherskirt")]: 3 } + { + items: [{ boostPercent: 3, itemID: itemID("Karil's leathertop") }], + gearSetup: 'wildy' + }, + { + items: [{ boostPercent: 3, itemID: itemID("Karil's leatherskirt") }], + gearSetup: 'wildy' + } ], defaultAttackStyles: [SkillsEnum.Ranged], combatXpMultiplier: 1.125, @@ -832,10 +853,17 @@ export const wildyKillableMonsters: KillableMonster[] = [ timeToFinish: Time.Minute * 2.9, emoji: '<:Fedora:456179157303427092>', wildy: true, - + canBePked: true, + pkActivityRating: 6, + pkBaseDeathChance: 7, difficultyRating: 6, qpRequired: 0, - itemInBankBoosts: [{ [itemID('Occult necklace')]: 10 }], + equippedItemBoosts: [ + { + items: [{ boostPercent: 10, itemID: itemID('Occult necklace') }], + gearSetup: 'wildy' + } + ], defaultAttackStyles: [SkillsEnum.Magic], combatXpMultiplier: 1.25, healAmountNeeded: 4 * 20, @@ -850,9 +878,21 @@ export const wildyKillableMonsters: KillableMonster[] = [ timeToFinish: Time.Minute * 3.0, emoji: '<:Scorpias_offspring:324127378773377024>', wildy: true, + canBePked: true, + pkActivityRating: 6, + pkBaseDeathChance: 7, difficultyRating: 7, qpRequired: 0, - itemInBankBoosts: [{ [itemID('Occult necklace')]: 10 }, { [itemID('Harmonised nightmare staff')]: 10 }], + equippedItemBoosts: [ + { + items: [{ boostPercent: 10, itemID: itemID('Occult necklace') }], + gearSetup: 'wildy' + }, + { + items: [{ boostPercent: 10, itemID: itemID('Harmonised nightmare staff') }], + gearSetup: 'wildy' + } + ], defaultAttackStyles: [SkillsEnum.Magic], combatXpMultiplier: 1.3, healAmountNeeded: 4 * 20, diff --git a/src/lib/minions/data/killableMonsters/chaeldarMonsters.ts b/src/lib/minions/data/killableMonsters/chaeldarMonsters.ts index 0c315a829f..6b4b9667de 100644 --- a/src/lib/minions/data/killableMonsters/chaeldarMonsters.ts +++ b/src/lib/minions/data/killableMonsters/chaeldarMonsters.ts @@ -32,12 +32,15 @@ export const chaeldarMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 60, table: Monsters.Aviansie, - wildy: false, + wildy: true, difficultyRating: 4, qpRequired: 0, defaultAttackStyles: [SkillsEnum.Ranged], disallowedAttackStyles: [SkillsEnum.Attack, SkillsEnum.Strength, SkillsEnum.Magic], - healAmountNeeded: 24 + healAmountNeeded: 24, + pkActivityRating: 7, + pkBaseDeathChance: 10, + revsWeaponBoost: true }, { id: Monsters.BlackDemon.id, @@ -45,7 +48,7 @@ export const chaeldarMonsters: KillableMonster[] = [ aliases: Monsters.BlackDemon.aliases, timeToFinish: Time.Second * 36, table: Monsters.BlackDemon, - wildy: false, + wildy: true, difficultyRating: 3, existsInCatacombs: true, @@ -64,7 +67,11 @@ export const chaeldarMonsters: KillableMonster[] = [ canCannon: true, // Even if no multi, can safespot for same effect cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 7, + pkBaseDeathChance: 9, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.CaveHorror.id, @@ -209,7 +216,7 @@ export const chaeldarMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 25, table: Monsters.GreaterDemon, - wildy: false, + wildy: true, existsInCatacombs: true, difficultyRating: 2, @@ -227,7 +234,11 @@ export const chaeldarMonsters: KillableMonster[] = [ attackStylesUsed: [GearStat.AttackSlash], canCannon: true, cannonMulti: true, - canBarrage: false + canBarrage: false, + pkActivityRating: 7, + pkBaseDeathChance: 9, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.IronDragon.id, diff --git a/src/lib/minions/data/killableMonsters/krystiliaMonsters.ts b/src/lib/minions/data/killableMonsters/krystiliaMonsters.ts index 3bfb7eda8c..c1967ce7a2 100644 --- a/src/lib/minions/data/killableMonsters/krystiliaMonsters.ts +++ b/src/lib/minions/data/killableMonsters/krystiliaMonsters.ts @@ -1,7 +1,7 @@ import { Time } from 'e'; import { Monsters } from 'oldschooljs'; -import resolveItems from '../../../util/resolveItems'; +import resolveItems, { deepResolveItems } from '../../../util/resolveItems'; import { KillableMonster } from '../../types'; export const krystiliaMonsters: KillableMonster[] = [ @@ -14,7 +14,11 @@ export const krystiliaMonsters: KillableMonster[] = [ wildy: true, difficultyRating: 5, - qpRequired: 0 + qpRequired: 0, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true, + wildyMulti: true }, { id: Monsters.ChaosDruid.id, @@ -28,8 +32,11 @@ export const krystiliaMonsters: KillableMonster[] = [ difficultyRating: 2, qpRequired: 0, canCannon: true, - cannonMulti: true, - canBarrage: false + cannonMulti: false, + canBarrage: false, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true }, { id: Monsters.DarkWarrior.id, @@ -44,7 +51,11 @@ export const krystiliaMonsters: KillableMonster[] = [ qpRequired: 0, canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true, + wildyMulti: true }, { id: Monsters.DeadlyRedSpider.id, @@ -59,7 +70,8 @@ export const krystiliaMonsters: KillableMonster[] = [ qpRequired: 0, canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + revsWeaponBoost: true }, { id: Monsters.ElderChaosDruid.id, @@ -74,7 +86,11 @@ export const krystiliaMonsters: KillableMonster[] = [ qpRequired: 0, canCannon: true, cannonMulti: true, - canBarrage: false + canBarrage: false, + pkActivityRating: 2, + pkBaseDeathChance: 7, + revsWeaponBoost: true, + wildyMulti: true }, { id: Monsters.Ent.id, @@ -86,11 +102,14 @@ export const krystiliaMonsters: KillableMonster[] = [ wildy: true, difficultyRating: 3, - itemsRequired: resolveItems(['Dragon axe', 'Rune axe']), + itemsRequired: deepResolveItems([['Dragon axe', 'Rune axe']]), qpRequired: 0, canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 2, + pkBaseDeathChance: 4, + revsWeaponBoost: true }, { id: Monsters.GuardBandit.id, @@ -105,7 +124,12 @@ export const krystiliaMonsters: KillableMonster[] = [ qpRequired: 0, canCannon: true, cannonMulti: true, - canBarrage: false + canBarrage: false, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true, + canBePked: true, + wildyMulti: true }, { id: Monsters.LavaDragon.id, @@ -119,7 +143,12 @@ export const krystiliaMonsters: KillableMonster[] = [ difficultyRating: 4, itemsRequired: resolveItems(['Anti-dragon shield']), notifyDrops: resolveItems(['Draconic visage']), - qpRequired: 0 + qpRequired: 0, + pkActivityRating: 3, + pkBaseDeathChance: 4, + revsWeaponBoost: true, + canBePked: true, + wildyMulti: true }, { id: Monsters.MagicAxe.id, @@ -131,11 +160,15 @@ export const krystiliaMonsters: KillableMonster[] = [ wildy: true, difficultyRating: 3, - // itemsRequired: resolveItems(['Lockpick']), + itemsRequired: resolveItems(['Lockpick']), qpRequired: 0, levelRequirements: { - // theiving: 23 - } + thieving: 23 + }, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true, + canCannon: true }, { id: Monsters.Mammoth.id, @@ -150,7 +183,12 @@ export const krystiliaMonsters: KillableMonster[] = [ qpRequired: 0, canCannon: true, cannonMulti: true, - canBarrage: false + canBarrage: false, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true, + canBePked: true, + wildyMulti: true }, { id: Monsters.Pirate.id, @@ -162,10 +200,13 @@ export const krystiliaMonsters: KillableMonster[] = [ wildy: true, difficultyRating: 3, - // itemsRequired: resolveItems(['Lockpick']), + itemsRequired: resolveItems(['Lockpick']), levelRequirements: { - // thieving: 39 + thieving: 39 }, - qpRequired: 0 + qpRequired: 0, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true } ]; diff --git a/src/lib/minions/data/killableMonsters/mazchnaMonsters.ts b/src/lib/minions/data/killableMonsters/mazchnaMonsters.ts index e7c6a0a5e8..b68e6ebc1b 100644 --- a/src/lib/minions/data/killableMonsters/mazchnaMonsters.ts +++ b/src/lib/minions/data/killableMonsters/mazchnaMonsters.ts @@ -86,7 +86,11 @@ export const mazchnaMonsters: KillableMonster[] = [ attackStylesUsed: [GearStat.AttackCrush], canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 4, + pkBaseDeathChance: 2, + revsWeaponBoost: true, + canBePked: true }, { id: Monsters.FeralVampyre.id, @@ -151,7 +155,7 @@ export const mazchnaMonsters: KillableMonster[] = [ aliases: Monsters.HillGiant.aliases, timeToFinish: Time.Second * 10, table: Monsters.HillGiant, - wildy: false, + wildy: true, existsInCatacombs: true, difficultyRating: 1, @@ -161,7 +165,10 @@ export const mazchnaMonsters: KillableMonster[] = [ attackStylesUsed: [GearStat.AttackCrush], canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 1, + pkBaseDeathChance: 5, + revsWeaponBoost: true }, { id: Monsters.Obor.id, @@ -211,7 +218,10 @@ export const mazchnaMonsters: KillableMonster[] = [ attackStylesUsed: [GearStat.AttackCrush], canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 4, + pkBaseDeathChance: 3, + revsWeaponBoost: true }, { id: Monsters.Killerwatt.id, diff --git a/src/lib/minions/data/killableMonsters/nieveMonsters.ts b/src/lib/minions/data/killableMonsters/nieveMonsters.ts index e9fee1abf4..92c3d65c7c 100644 --- a/src/lib/minions/data/killableMonsters/nieveMonsters.ts +++ b/src/lib/minions/data/killableMonsters/nieveMonsters.ts @@ -32,7 +32,7 @@ export const nieveMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 76, table: Monsters.BlackDragon, - wildy: false, + wildy: true, difficultyRating: 4, itemsRequired: resolveItems(['Anti-dragon shield']), @@ -42,7 +42,11 @@ export const nieveMonsters: KillableMonster[] = [ attackStylesUsed: [GearStat.AttackSlash], canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 2, + pkBaseDeathChance: 7, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.BrutalBlackDragon.id, diff --git a/src/lib/minions/data/killableMonsters/revs.ts b/src/lib/minions/data/killableMonsters/revs.ts index a08f3f8939..a95a3cf52e 100644 --- a/src/lib/minions/data/killableMonsters/revs.ts +++ b/src/lib/minions/data/killableMonsters/revs.ts @@ -74,7 +74,7 @@ export const revenantMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 147, table: Monsters.RevenantDragon, wildy: true, - difficultyRating: 9, + difficultyRating: 10, qpRequired: 0, pkActivityRating: 9, pkBaseDeathChance: 8, diff --git a/src/lib/minions/data/killableMonsters/turaelMonsters.ts b/src/lib/minions/data/killableMonsters/turaelMonsters.ts index c7ea433b62..79a1121db5 100644 --- a/src/lib/minions/data/killableMonsters/turaelMonsters.ts +++ b/src/lib/minions/data/killableMonsters/turaelMonsters.ts @@ -455,7 +455,9 @@ export const turaelMonsters: KillableMonster[] = [ qpRequired: 0, canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 1, + pkBaseDeathChance: 1 }, { id: Monsters.Goblin.id, @@ -483,7 +485,10 @@ export const turaelMonsters: KillableMonster[] = [ qpRequired: 0, canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 8, + pkBaseDeathChance: 4, + revsWeaponBoost: true }, { id: Monsters.GrizzlyBearCub.id, @@ -840,7 +845,7 @@ export const turaelMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 10, table: Monsters.Scorpion, - wildy: false, + wildy: true, difficultyRating: 1, qpRequired: 0, @@ -849,7 +854,10 @@ export const turaelMonsters: KillableMonster[] = [ canBarrage: false, healAmountNeeded: 8, attackStyleToUse: GearStat.AttackSlash, - attackStylesUsed: [GearStat.AttackCrush] + attackStylesUsed: [GearStat.AttackCrush], + pkActivityRating: 3, + pkBaseDeathChance: 2, + revsWeaponBoost: true }, { id: Monsters.Seagull.id, @@ -890,7 +898,7 @@ export const turaelMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 10, table: Monsters.Skeleton, - wildy: false, + wildy: true, existsInCatacombs: true, difficultyRating: 1, @@ -900,7 +908,10 @@ export const turaelMonsters: KillableMonster[] = [ canBarrage: false, healAmountNeeded: 11, attackStyleToUse: GearStat.AttackSlash, - attackStylesUsed: [GearStat.AttackCrush] + attackStylesUsed: [GearStat.AttackCrush], + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true }, { id: Monsters.SkeletonFremennik.id, @@ -971,13 +982,16 @@ export const turaelMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 5, table: Monsters.Spider, - wildy: false, + wildy: true, difficultyRating: 1, qpRequired: 0, canCannon: true, cannonMulti: false, - canBarrage: false + canBarrage: false, + pkActivityRating: 1, + pkBaseDeathChance: 1, + revsWeaponBoost: true }, { id: Monsters.SulphurLizard.id, @@ -1200,7 +1214,7 @@ export const turaelMonsters: KillableMonster[] = [ aliases: Monsters.Zombie.aliases, timeToFinish: Time.Second * 10, table: Monsters.Zombie, - wildy: false, + wildy: true, difficultyRating: 1, qpRequired: 0, @@ -1209,7 +1223,10 @@ export const turaelMonsters: KillableMonster[] = [ canBarrage: false, healAmountNeeded: 9, attackStyleToUse: GearStat.AttackSlash, - attackStylesUsed: [GearStat.AttackCrush] + attackStylesUsed: [GearStat.AttackCrush], + pkActivityRating: 6, + pkBaseDeathChance: 4, + revsWeaponBoost: true }, { id: Monsters.ZombieRat.id, diff --git a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts index 3e1b739de1..61744f0afd 100644 --- a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts +++ b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts @@ -38,7 +38,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 29, table: Monsters.AbyssalDemon, - wildy: false, + wildy: true, difficultyRating: 3, qpRequired: 0, @@ -58,7 +58,11 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 35, attackStyleToUse: GearStat.AttackSlash, attackStylesUsed: [GearStat.AttackStab], - canBarrage: true + canBarrage: true, + pkActivityRating: 7, + pkBaseDeathChance: 10, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.AbyssalSire.id, @@ -119,7 +123,11 @@ export const vannakaMonsters: KillableMonster[] = [ [itemID('Kodai wand')]: 12, [itemID('Staff of the dead')]: 8 } - ] + ], + pkActivityRating: 4, + pkBaseDeathChance: 3, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.BabyBlueDragon.id, @@ -208,7 +216,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 27, table: Monsters.Bloodveld, - wildy: false, + wildy: true, difficultyRating: 1, qpRequired: 0, @@ -236,7 +244,10 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 12, attackStyleToUse: GearStat.AttackRanged, attackStylesUsed: [GearStat.AttackMagic], - canCannon: true + canCannon: true, + pkActivityRating: 4, + pkBaseDeathChance: 6, + revsWeaponBoost: true }, { id: Monsters.BlueDragon.id, @@ -439,7 +450,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 18, table: Monsters.DustDevil, - wildy: false, + wildy: true, difficultyRating: 2, existsInCatacombs: true, @@ -460,7 +471,11 @@ export const vannakaMonsters: KillableMonster[] = [ canBarrage: true, healAmountNeeded: 16, attackStyleToUse: GearStat.AttackSlash, - attackStylesUsed: [GearStat.AttackCrush] + attackStylesUsed: [GearStat.AttackCrush], + pkActivityRating: 6, + pkBaseDeathChance: 8, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.ElfArcher.id, @@ -532,7 +547,10 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 17, attackStyleToUse: GearStat.AttackSlash, attackStylesUsed: [GearStat.AttackSlash], - canCannon: true + canCannon: true, + pkActivityRating: 3, + pkBaseDeathChance: 8, + revsWeaponBoost: true }, { id: Monsters.Gargoyle.id, @@ -617,7 +635,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 37.2, table: Monsters.GreaterNechryael, - wildy: false, + wildy: true, difficultyRating: 5, qpRequired: 0, @@ -637,7 +655,11 @@ export const vannakaMonsters: KillableMonster[] = [ attackStyleToUse: GearStat.AttackSlash, attackStylesUsed: [GearStat.AttackCrush], canBarrage: true, - canCannon: true + canCannon: true, + pkActivityRating: 8, + pkBaseDeathChance: 9, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.GreenDragon.id, @@ -654,7 +676,11 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 20, attackStyleToUse: GearStat.AttackSlash, attackStylesUsed: [GearStat.AttackSlash], - canCannon: true + canCannon: true, + revsWeaponBoost: true, + wildySlayerCave: true, + pkActivityRating: 2, + pkBaseDeathChance: 4 }, { id: Monsters.HarpieBugSwarm.id, @@ -682,7 +708,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 39, table: Monsters.Hellhound, - wildy: false, + wildy: true, existsInCatacombs: true, difficultyRating: 3, @@ -699,7 +725,11 @@ export const vannakaMonsters: KillableMonster[] = [ canCannon: true, // Not multi but you can safespot for the same effect cannonMulti: true, - canBarrage: false + canBarrage: false, + pkActivityRating: 5, + pkBaseDeathChance: 8, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.IceGiant.id, @@ -716,8 +746,12 @@ export const vannakaMonsters: KillableMonster[] = [ attackStyleToUse: GearStat.AttackSlash, attackStylesUsed: [GearStat.AttackSlash], canCannon: true, - cannonMulti: false, - canBarrage: false + cannonMulti: true, + canBarrage: false, + pkActivityRating: 2, + pkBaseDeathChance: 6, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.IceTroll.id, @@ -799,7 +833,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 25, table: Monsters.Jelly, - wildy: false, + wildy: true, difficultyRating: 2, qpRequired: 0, @@ -809,7 +843,11 @@ export const vannakaMonsters: KillableMonster[] = [ superior: Monsters.VitreousJelly, healAmountNeeded: 14, attackStyleToUse: GearStat.AttackRanged, - attackStylesUsed: [GearStat.AttackMagic] + attackStylesUsed: [GearStat.AttackMagic], + pkActivityRating: 6, + pkBaseDeathChance: 8, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.JungleHorror.id, @@ -877,7 +915,11 @@ export const vannakaMonsters: KillableMonster[] = [ canCannon: true, // No multi spots (i think) but you can safespot for same effect. cannonMulti: true, - canBarrage: false + canBarrage: false, + pkActivityRating: 7, + pkBaseDeathChance: 9, + revsWeaponBoost: true, + wildySlayerCave: true }, { id: Monsters.Molanisk.id, @@ -912,7 +954,10 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 17, attackStyleToUse: GearStat.AttackSlash, attackStylesUsed: [GearStat.AttackSlash], - canCannon: true + canCannon: true, + pkActivityRating: 4, + pkBaseDeathChance: 3, + revsWeaponBoost: true }, { id: Monsters.Bryophyta.id, @@ -1145,7 +1190,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 15, table: Monsters.SpiritualMage, - wildy: false, + wildy: true, difficultyRating: 4, qpRequired: 0, @@ -1154,7 +1199,10 @@ export const vannakaMonsters: KillableMonster[] = [ }, healAmountNeeded: 27, attackStyleToUse: GearStat.AttackRanged, - attackStylesUsed: [GearStat.AttackMagic] + attackStylesUsed: [GearStat.AttackMagic], + pkActivityRating: 4, + pkBaseDeathChance: 6, + revsWeaponBoost: true }, { id: Monsters.SpiritualRanger.id, @@ -1163,7 +1211,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 18, table: Monsters.SpiritualRanger, - wildy: false, + wildy: true, difficultyRating: 3, qpRequired: 0, @@ -1172,7 +1220,10 @@ export const vannakaMonsters: KillableMonster[] = [ }, healAmountNeeded: 25, attackStyleToUse: GearStat.AttackSlash, - attackStylesUsed: [GearStat.AttackRanged] + attackStylesUsed: [GearStat.AttackRanged], + pkActivityRating: 4, + pkBaseDeathChance: 6, + revsWeaponBoost: true }, { id: Monsters.SpiritualWarrior.id, @@ -1181,7 +1232,7 @@ export const vannakaMonsters: KillableMonster[] = [ timeToFinish: Time.Second * 19, table: Monsters.SpiritualWarrior, - wildy: false, + wildy: true, difficultyRating: 3, qpRequired: 0, @@ -1190,7 +1241,10 @@ export const vannakaMonsters: KillableMonster[] = [ }, healAmountNeeded: 26, attackStyleToUse: GearStat.AttackSlash, - attackStylesUsed: [GearStat.AttackSlash] + attackStylesUsed: [GearStat.AttackSlash], + pkActivityRating: 4, + pkBaseDeathChance: 6, + revsWeaponBoost: true }, { id: Monsters.TerrorDog.id, diff --git a/src/lib/minions/types.ts b/src/lib/minions/types.ts index dce2ebbdca..6f48bfe698 100644 --- a/src/lib/minions/types.ts +++ b/src/lib/minions/types.ts @@ -71,6 +71,7 @@ export interface KillableMonster { existsInCatacombs?: boolean; qpRequired?: number; difficultyRating?: number; + revsWeaponBoost?: boolean; /** * An array of objects of ([key: itemID]: boostPercentage) boosts that apply to @@ -136,6 +137,7 @@ export interface KillableMonster { requiredQuests?: QuestID[]; deathProps?: Omit['0'], 'currentKC'>; diaryRequirement?: [Diary, DiaryTier]; + wildySlayerCave?: boolean; requiredBitfield?: BitField; minimumFoodHealAmount?: number; diff --git a/src/lib/openables.ts b/src/lib/openables.ts index 31360b1a80..7b6fda8024 100644 --- a/src/lib/openables.ts +++ b/src/lib/openables.ts @@ -16,6 +16,7 @@ import { clueHunterOutfit } from './data/CollectionsExport'; import { defaultFarmingContract } from './minions/farming'; import { FarmingContract } from './minions/farming/types'; import { shadeChestOpenables } from './shadesKeys'; +import { nestTable } from './simulation/birdsNest'; import { BagFullOfGemsTable, BuildersSupplyCrateTable, @@ -368,6 +369,14 @@ const osjsOpenables: UnifiedOpenable[] = [ output: Openables.NestBoxSeeds.table, allItems: Openables.NestBoxSeeds.table.allItems }, + { + name: 'Bird nest', + id: 5070, + openedItem: getOSItem(5070), + aliases: ['bird nest', 'nest'], + output: nestTable, + allItems: nestTable.allItems + }, { name: 'Ogre coffin', id: 4850, diff --git a/src/lib/settings/prisma.ts b/src/lib/settings/prisma.ts index e8c6096614..d8ea48c39a 100644 --- a/src/lib/settings/prisma.ts +++ b/src/lib/settings/prisma.ts @@ -17,6 +17,10 @@ declare global { function makePrismaClient(): PrismaClient { if (!isMainThread && !process.env.TEST) return null as any; if (!production && !process.env.TEST) console.log('Making prisma client...'); + if (!isMainThread) { + throw new Error('Prisma client should only be created on the main thread.'); + } + return new PrismaClient({ log: [ { diff --git a/src/lib/skilling/functions/miningBoosts.ts b/src/lib/skilling/functions/miningBoosts.ts index cb3ab9df18..4fd89bb056 100644 --- a/src/lib/skilling/functions/miningBoosts.ts +++ b/src/lib/skilling/functions/miningBoosts.ts @@ -1,5 +1,3 @@ -import { Bank } from 'oldschooljs'; - import itemID from '../../util/itemID'; export const pickaxes = [ @@ -65,10 +63,10 @@ export const pickaxes = [ } ]; -export const miningGloves = [ +export const miningGloves: { id: number; Percentages: Record }[] = [ { id: itemID('Expert mining gloves'), - Percentages: new Bank({ + Percentages: { 'Silver ore': 50, Coal: 40, 'Gold ore': 33.33, @@ -76,11 +74,11 @@ export const miningGloves = [ 'Adamantite ore': 16.66, 'Runite ore': 12.5, Amethyst: 25 - }) + } }, { id: itemID('Superior mining gloves'), - Percentages: new Bank({ + Percentages: { 'Silver ore': 0, Coal: 0, 'Gold ore': 0, @@ -88,11 +86,11 @@ export const miningGloves = [ 'Adamantite ore': 16.66, 'Runite ore': 12.5, Amethyst: 0 - }) + } }, { id: itemID('Mining gloves'), - Percentages: new Bank({ + Percentages: { 'Silver ore': 50, Coal: 40, 'Gold ore': 33.33, @@ -100,14 +98,14 @@ export const miningGloves = [ 'Adamantite ore': 0, 'Runite ore': 0, Amethyst: 0 - }) + } } ]; -export const varrockArmours = [ +export const varrockArmours: { id: number; Percentages: Record }[] = [ { id: itemID('Varrock armour 4'), - Percentages: new Bank({ + Percentages: { Clay: 10, 'Copper ore': 10, 'Tin ore': 10, @@ -121,11 +119,11 @@ export const varrockArmours = [ 'Adamantite ore': 10, 'Runite ore': 10, Amethyst: 10 - }) + } }, { id: itemID('Varrock armour 3'), - Percentages: new Bank({ + Percentages: { Clay: 10, 'Copper ore': 10, 'Tin ore': 10, @@ -139,11 +137,11 @@ export const varrockArmours = [ 'Adamantite ore': 10, 'Runite ore': 0, Amethyst: 0 - }) + } }, { id: itemID('Varrock armour 2'), - Percentages: new Bank({ + Percentages: { Clay: 10, 'Copper ore': 10, 'Tin ore': 10, @@ -157,11 +155,11 @@ export const varrockArmours = [ 'Adamantite ore': 0, 'Runite ore': 0, Amethyst: 0 - }) + } }, { id: itemID('Varrock armour 1'), - Percentages: new Bank({ + Percentages: { Clay: 10, 'Copper ore': 10, 'Tin ore': 10, @@ -175,11 +173,11 @@ export const varrockArmours = [ 'Adamantite ore': 0, 'Runite ore': 0, Amethyst: 0 - }) + } } ]; -export const miningCapeOreEffect: Bank = new Bank({ +export const miningCapeOreEffect: Record = { Clay: 5, 'Copper ore': 5, 'Tin ore': 5, @@ -193,4 +191,4 @@ export const miningCapeOreEffect: Bank = new Bank({ 'Adamantite ore': 5, 'Runite ore': 0, Amethyst: 0 -}); +}; diff --git a/src/lib/skilling/skills/herblore/mixables/barbMixes.ts b/src/lib/skilling/skills/herblore/mixables/barbMixes.ts index adb2cecbf0..a25777d7c5 100644 --- a/src/lib/skilling/skills/herblore/mixables/barbMixes.ts +++ b/src/lib/skilling/skills/herblore/mixables/barbMixes.ts @@ -33,10 +33,7 @@ export const barbMixes: Mixable[] = [ aliases: ['Relicyms mix roe', 'Relicyms mix(2)', 'Relicyms mix 2 roe'], level: 9, xp: 14, - inputItems: new Bank({ - 4846: 1, - Roe: 1 - }), + inputItems: new Bank().add("Relicym's balm(2)").add('Roe'), tickRate: 1, bankTimePerPotion: 0.088 }, diff --git a/src/lib/slayer/constants.ts b/src/lib/slayer/constants.ts index 512f87350f..c3b327a990 100644 --- a/src/lib/slayer/constants.ts +++ b/src/lib/slayer/constants.ts @@ -12,7 +12,8 @@ export const slayerMasterChoices = [ 'Nieve', 'Chaeldar', 'Mazchna', - 'Turael' + 'Turael', + 'Krystilia' ].map(smc => { return { name: smc, value: smc }; }); diff --git a/src/lib/slayer/slayerMasters.ts b/src/lib/slayer/slayerMasters.ts index 91244d9c25..058196d72e 100644 --- a/src/lib/slayer/slayerMasters.ts +++ b/src/lib/slayer/slayerMasters.ts @@ -3,6 +3,7 @@ import { MonsterSlayerMaster } from 'oldschooljs'; import { chaeldarTasks } from './tasks/chaeldarTasks'; import { duradelTasks } from './tasks/duradelTasks'; import { konarTasks } from './tasks/konarTasks'; +import { krystiliaTasks } from './tasks/krystiliaTasks'; import { mazchnaTasks } from './tasks/mazchnaTasks'; import { nieveTasks } from './tasks/nieveTasks'; import { turaelTasks } from './tasks/turaelTasks'; @@ -77,5 +78,13 @@ export const slayerMasters: SlayerMaster[] = [ combatLvl: 100, slayerLvl: 50, osjsEnum: MonsterSlayerMaster.Duradel + }, + { + id: 8, + name: 'Krystilia', + aliases: ['krystilia', 'wildy slayer', 'wilderness slayer'], + tasks: krystiliaTasks, + basePoints: 25, + osjsEnum: MonsterSlayerMaster.Krystilia } ]; diff --git a/src/lib/slayer/slayerUnlocks.ts b/src/lib/slayer/slayerUnlocks.ts index 0799b9c88b..d0ae017530 100644 --- a/src/lib/slayer/slayerUnlocks.ts +++ b/src/lib/slayer/slayerUnlocks.ts @@ -36,6 +36,7 @@ export enum SlayerTaskUnlocksEnum { StopTheWyvern, Basilocked, ActualVampyreSlayer, + IWildyMoreSlayer, // Extension Unlocks NeedMoreDarkness, AnkouVeryMuch, @@ -62,6 +63,7 @@ export enum SlayerTaskUnlocksEnum { WyverNotherTwo, Basilonger, MoreAtStake, + Revenenenenenants, // Item Purchases: SlayerRing, HerbSack, @@ -227,6 +229,14 @@ export const SlayerRewardsShop: SlayerTaskUnlocks[] = [ canBeRemoved: true, aliases: ['vampyre slayer', 'vampire slayer', 'actual vampire slayer', 'vampyres', 'vampires'] }, + { + id: SlayerTaskUnlocksEnum.IWildyMoreSlayer, + name: 'I Wildy More Slayer', + desc: 'Krystilia will be able to assign Jellies, Dust Devils, Nechryaels and Abyssal Demons as your task.', + slayerPointCost: 0, + canBeRemoved: true, + aliases: ['wildy slayer'] + }, { id: SlayerTaskUnlocksEnum.SlayerRing, name: 'Slayer ring', @@ -520,6 +530,16 @@ export const SlayerRewardsShop: SlayerTaskUnlocks[] = [ canBeRemoved: false, aliases: ['broad bolts', 'broads', 'broad arrows', 'fletching', 'broad fletching'] }, + { + id: SlayerTaskUnlocksEnum.Revenenenenenants, + name: 'Revenenenenenants', + desc: 'Extends Revenants tasks', + slayerPointCost: 100, + extendID: [Monsters.RevenantImp.id], + extendMult: 1.5, + canBeRemoved: true, + aliases: ['extend revenants', 'extend revs'] + }, { id: SlayerTaskUnlocksEnum.SizeMatters, name: 'Size Matters', diff --git a/src/lib/slayer/slayerUtil.ts b/src/lib/slayer/slayerUtil.ts index af5d0a481c..9ab5efa69a 100644 --- a/src/lib/slayer/slayerUtil.ts +++ b/src/lib/slayer/slayerUtil.ts @@ -19,7 +19,7 @@ import { autoslayModes } from './constants'; import { slayerMasters } from './slayerMasters'; import { SlayerRewardsShop, SlayerTaskUnlocksEnum } from './slayerUnlocks'; import { allSlayerTasks } from './tasks'; -import { bossTasks } from './tasks/bossTasks'; +import { bossTasks, wildernessBossTasks } from './tasks/bossTasks'; import { AssignableSlayerTask, SlayerMaster } from './types'; export enum SlayerMasterEnum { @@ -39,6 +39,7 @@ export interface DetermineBoostParams { monster: KillableMonster; method?: PvMMethod | null; isOnTask?: boolean; + wildyBurst?: boolean; } export function determineBoostChoice(params: DetermineBoostParams) { let boostChoice = 'none'; @@ -57,9 +58,15 @@ export function determineBoostChoice(params: DetermineBoostParams) { boostChoice = 'burst'; } else if (params.method && params.method === 'cannon') { boostChoice = 'cannon'; - } else if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage) && params.monster!.canBarrage) { + } else if ( + params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage) && + (params.monster!.canBarrage || params.wildyBurst) + ) { boostChoice = 'barrage'; - } else if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst) && params.monster!.canBarrage) { + } else if ( + params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst) && + (params.monster!.canBarrage || params.wildyBurst) + ) { boostChoice = 'burst'; } else if (params.cbOpts.includes(CombatOptionsEnum.AlwaysCannon)) { boostChoice = 'cannon'; @@ -180,6 +187,12 @@ export function userCanUseTask( !myUnlocks.includes(SlayerTaskUnlocksEnum.Basilocked) ) return false; + if ( + (lmon === 'dust devil' || lmon === 'greater nechryael' || lmon === 'abyssal demon' || lmon === 'jelly') && + lmast === 'krystilia' && + !myUnlocks.includes(SlayerTaskUnlocksEnum.IWildyMoreSlayer) + ) + return false; return true; } @@ -188,6 +201,7 @@ export async function assignNewSlayerTask(_user: MUser, master: SlayerMaster) { // assignedTask is the task object, currentTask is the database row. const baseTasks = [...master.tasks].filter(t => userCanUseTask(_user, t, master, false)); let bossTask = false; + let wildyBossTask = false; if ( _user.user.slayer_unlocks.includes(SlayerTaskUnlocksEnum.LikeABoss) && (master.name.toLowerCase() === 'konar quo maten' || @@ -199,15 +213,27 @@ export async function assignNewSlayerTask(_user: MUser, master: SlayerMaster) { bossTask = true; } + if (_user.user.slayer_unlocks.includes(SlayerTaskUnlocksEnum.LikeABoss) && master.id === 8 && roll(25)) { + wildyBossTask = true; + } + let assignedTask: AssignableSlayerTask | null = null; + if (bossTask) { const baseBossTasks = bossTasks.filter(t => userCanUseTask(_user, t, master, true)); if (baseBossTasks.length > 0) { assignedTask = weightedPick(baseBossTasks); - } else { - assignedTask = weightedPick(baseTasks); } - } else { + } + + if (wildyBossTask) { + const baseWildyBossTasks = wildernessBossTasks.filter(t => userCanUseTask(_user, t, master, true)); + if (baseWildyBossTasks.length > 0) { + assignedTask = weightedPick(baseWildyBossTasks); + } + } + + if (assignedTask === null) { assignedTask = weightedPick(baseTasks); } @@ -313,6 +339,9 @@ export function getCommonTaskName(task: Monster) { case Monsters.TzHaarKet.id: commonName = 'TzHaar'; break; + case Monsters.RevenantImp.id: + commonName = 'Revenant'; + break; default: } if (commonName !== 'TzHaar' && !commonName.endsWith('s')) commonName += 's'; diff --git a/src/lib/slayer/tasks/bossTasks.ts b/src/lib/slayer/tasks/bossTasks.ts index bac6139f2a..eca159c490 100644 --- a/src/lib/slayer/tasks/bossTasks.ts +++ b/src/lib/slayer/tasks/bossTasks.ts @@ -36,7 +36,8 @@ export const bossTasks: AssignableSlayerTask[] = [ amount: [3, 35], weight: 1, monsters: [Monsters.Callisto.id], - isBoss: true + isBoss: true, + wilderness: true }, { monster: Monsters.Cerberus, @@ -54,14 +55,16 @@ export const bossTasks: AssignableSlayerTask[] = [ amount: [3, 35], weight: 1, monsters: [Monsters.ChaosElemental.id], - isBoss: true + isBoss: true, + wilderness: true }, { monster: Monsters.ChaosFanatic, amount: [3, 35], weight: 1, monsters: [Monsters.ChaosFanatic.id], - isBoss: true + isBoss: true, + wilderness: true }, { monster: Monsters.CommanderZilyana, @@ -79,7 +82,8 @@ export const bossTasks: AssignableSlayerTask[] = [ amount: [3, 35], weight: 1, monsters: [Monsters.CrazyArchaeologist.id], - isBoss: true + isBoss: true, + wilderness: true }, { monster: Monsters.DagannothPrime, @@ -202,7 +206,8 @@ export const bossTasks: AssignableSlayerTask[] = [ amount: [3, 35], weight: 1, monsters: [Monsters.Scorpia.id], - isBoss: true + isBoss: true, + wilderness: true }, { monster: Monsters.ThermonuclearSmokeDevil, @@ -217,14 +222,16 @@ export const bossTasks: AssignableSlayerTask[] = [ amount: [3, 35], weight: 1, monsters: [Monsters.Venenatis.id], - isBoss: true + isBoss: true, + wilderness: true }, { monster: Monsters.Vetion, amount: [3, 35], weight: 1, monsters: [Monsters.Vetion.id], - isBoss: true + isBoss: true, + wilderness: true }, { monster: Monsters.Vorkath, @@ -262,3 +269,62 @@ export const bossTasks: AssignableSlayerTask[] = [ isBoss: true } ]; + +export const wildernessBossTasks: AssignableSlayerTask[] = [ + { + monster: Monsters.Callisto, + amount: [3, 35], + weight: 1, + monsters: [Monsters.Callisto.id], + isBoss: true, + wilderness: true + }, + { + monster: Monsters.ChaosElemental, + amount: [3, 35], + weight: 1, + monsters: [Monsters.ChaosElemental.id], + isBoss: true, + wilderness: true + }, + { + monster: Monsters.ChaosFanatic, + amount: [3, 35], + weight: 1, + monsters: [Monsters.ChaosFanatic.id], + isBoss: true, + wilderness: true + }, + { + monster: Monsters.CrazyArchaeologist, + amount: [3, 35], + weight: 1, + monsters: [Monsters.CrazyArchaeologist.id], + isBoss: true, + wilderness: true + }, + { + monster: Monsters.Scorpia, + amount: [3, 35], + weight: 1, + monsters: [Monsters.Scorpia.id], + isBoss: true, + wilderness: true + }, + { + monster: Monsters.Venenatis, + amount: [3, 35], + weight: 1, + monsters: [Monsters.Venenatis.id], + isBoss: true, + wilderness: true + }, + { + monster: Monsters.Vetion, + amount: [3, 35], + weight: 1, + monsters: [Monsters.Vetion.id], + isBoss: true, + wilderness: true + } +]; diff --git a/src/lib/slayer/tasks/krystiliaTasks.ts b/src/lib/slayer/tasks/krystiliaTasks.ts new file mode 100644 index 0000000000..ffc05fecaa --- /dev/null +++ b/src/lib/slayer/tasks/krystiliaTasks.ts @@ -0,0 +1,333 @@ +import { Monsters } from 'oldschooljs'; + +import { SlayerTaskUnlocksEnum } from '../slayerUnlocks'; +import { AssignableSlayerTask } from '../types'; +import { wildernessBossTasks } from './bossTasks'; + +export const krystiliaTasks: AssignableSlayerTask[] = [ + { + monster: Monsters.AbyssalDemon, + amount: [75, 125], + weight: 5, + monsters: [Monsters.AbyssalDemon.id], + extendedAmount: [200, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.AugmentMyAbbies, + slayerLevel: 85, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Ankou, + amount: [75, 125], + weight: 6, + monsters: [Monsters.Ankou.id], + extendedAmount: [91, 150], + extendedUnlockId: SlayerTaskUnlocksEnum.AnkouVeryMuch, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Aviansie, + amount: [75, 125], + weight: 7, + monsters: [Monsters.Aviansie.id], + extendedAmount: [200, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.BirdsOfAFeather, + unlocked: true, + wilderness: true + }, + // { + // monster: Monsters.Bandit, + // amount: [75, 125], + // weight: 4, + // monsters: [Monsters.Bandit.id], + // unlocked: true, + // wilderness: true + // }, + { + monster: Monsters.GrizzlyBear, + amount: [65, 100], + weight: 6, + monsters: [Monsters.GrizzlyBear.id, Monsters.Artio.id, Monsters.Callisto.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.BlackDemon, + amount: [100, 150], + weight: 7, + monsters: [Monsters.BlackDemon.id], + extendedAmount: [200, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.ItsDarkInHere, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.BlackDragon, + amount: [8, 16], + weight: 4, + monsters: [Monsters.BlackDragon.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.BlackKnight, + amount: [75, 125], + weight: 3, + monsters: [Monsters.BlackKnight.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Bloodveld, + amount: [70, 110], + weight: 4, + monsters: [Monsters.Bloodveld.id], + extendedAmount: [200, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.BleedMeDry, + slayerLevel: 50, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.ChaosDruid, + amount: [50, 90], + weight: 5, + monsters: [Monsters.ChaosDruid.id, Monsters.ElderChaosDruid.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.DarkWarrior, + amount: [75, 125], + weight: 4, + monsters: [Monsters.DarkWarrior.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.DustDevil, + amount: [75, 125], + weight: 5, + monsters: [Monsters.DustDevil.id], + extendedAmount: [200, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.ToDustYouShallReturn, + slayerLevel: 65, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.EarthWarrior, + amount: [75, 125], + weight: 6, + monsters: [Monsters.EarthWarrior.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Ent, + amount: [35, 60], + weight: 5, + monsters: [Monsters.Ent.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.FireGiant, + amount: [75, 125], + weight: 7, + monsters: [Monsters.FireGiant.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.GreaterDemon, + amount: [100, 150], + weight: 8, + monsters: [Monsters.GreaterDemon.id], + extendedAmount: [200, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.GreaterChallenge, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.GreenDragon, + amount: [65, 100], + weight: 4, + monsters: [Monsters.GreenDragon.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Hellhound, + amount: [75, 125], + weight: 7, + monsters: [Monsters.Hellhound.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.HillGiant, + amount: [75, 125], + weight: 3, + monsters: [Monsters.HillGiant.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.IceGiant, + amount: [100, 150], + weight: 6, + monsters: [Monsters.IceGiant.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.IceWarrior, + amount: [100, 150], + weight: 7, + monsters: [Monsters.IceWarrior.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Jelly, + amount: [100, 150], + weight: 5, + monsters: [Monsters.Jelly.id], + slayerLevel: 52, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.LavaDragon, + amount: [35, 60], + weight: 3, + monsters: [Monsters.LavaDragon.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.LesserDemon, + amount: [80, 120], + weight: 6, + monsters: [Monsters.LesserDemon.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.MagicAxe, + amount: [75, 125], + weight: 7, + monsters: [Monsters.MagicAxe.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Mammoth, + amount: [75, 125], + weight: 6, + monsters: [Monsters.Mammoth.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.MossGiant, + amount: [100, 150], + weight: 4, + monsters: [Monsters.MossGiant.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.GreaterNechryael, + amount: [75, 125], + weight: 5, + monsters: [Monsters.GreaterNechryael.id], + slayerLevel: 80, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Pirate, + amount: [62, 75], + weight: 3, + monsters: [Monsters.Pirate.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.RevenantImp, + amount: [40, 100], + weight: 5, + monsters: [ + Monsters.RevenantCyclops.id, + Monsters.RevenantDarkBeast.id, + Monsters.RevenantDemon.id, + Monsters.RevenantDragon.id, + Monsters.RevenantGoblin.id, + Monsters.RevenantHellhound.id, + Monsters.RevenantHobgoblin.id, + Monsters.RevenantImp.id, + Monsters.RevenantKnight.id, + Monsters.RevenantOrk.id, + Monsters.RevenantPyrefiend.id + ], + extendedAmount: [100, 150], + extendedUnlockId: SlayerTaskUnlocksEnum.Revenenenenenants, + unlocked: true, + wilderness: true + }, + // { + // monster: Monsters.Rogue, + // amount: [75, 125], + // weight: 5, + // monsters: [Monsters.Rogue.id], + // unlocked: true, + // wilderness: true + // }, + { + monster: Monsters.Scorpion, + amount: [65, 100], + weight: 6, + monsters: [Monsters.Scorpia.id, Monsters.Scorpion.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Skeleton, + amount: [65, 100], + weight: 5, + monsters: [Monsters.Skeleton.id, Monsters.Vetion.id, Monsters.Calvarion.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Spider, + amount: [65, 100], + weight: 6, + monsters: [Monsters.Spider.id, Monsters.Venenatis.id, Monsters.Spindel.id], + unlocked: true, + wilderness: true + }, + { + monster: Monsters.SpiritualRanger, + amount: [100, 150], + weight: 6, + monsters: [Monsters.SpiritualMage.id, Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id], + extendedAmount: [181, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.SpiritualFervour, + slayerLevel: 63, + unlocked: true, + wilderness: true + }, + { + monster: Monsters.Zombie, + amount: [75, 125], + weight: 3, + monsters: [Monsters.Zombie.id], + unlocked: true, + wilderness: true + }, + ...wildernessBossTasks +]; diff --git a/src/lib/slayer/types.ts b/src/lib/slayer/types.ts index 2a1bffb60e..103c323840 100644 --- a/src/lib/slayer/types.ts +++ b/src/lib/slayer/types.ts @@ -18,6 +18,7 @@ export interface AssignableSlayerTask { dontAssign?: boolean; extendedAmount?: [number, number]; extendedUnlockId?: number; + wilderness?: boolean; dungeoneeringLevel?: number; } diff --git a/src/lib/types/minions.ts b/src/lib/types/minions.ts index d9821df1d8..e57b8fa13b 100644 --- a/src/lib/types/minions.ts +++ b/src/lib/types/minions.ts @@ -159,6 +159,7 @@ export interface MonsterActivityTaskOptions extends ActivityTaskOptions { died?: boolean; pkEncounters?: number; hasWildySupplies?: boolean; + isInWilderness?: boolean; } export interface ClueActivityTaskOptions extends ActivityTaskOptions { diff --git a/src/lib/util/calcWildyPkChance.ts b/src/lib/util/calcWildyPkChance.ts index ef35b14d62..016e9ad914 100644 --- a/src/lib/util/calcWildyPkChance.ts +++ b/src/lib/util/calcWildyPkChance.ts @@ -46,7 +46,8 @@ export async function calcWildyPKChance( peak: Peak, monster: KillableMonster, duration: number, - supplies: boolean + supplies: boolean, + cannonMulti: boolean ): Promise<[number, boolean, string]> { // Chance per minute, Difficulty from 1 to 10, and factor a million difference, High peak 5x as likley encounter, Medium peak 1x, Low peak 5x as unlikley const peakInfluence = peakFactor.find(_peaktier => _peaktier.peakTier === peak?.peakTier)?.factor ?? 1; @@ -72,7 +73,7 @@ export async function calcWildyPKChance( deathChance += deathChanceFromLevels; // Multi does make it riskier, but only if there's actually a team on you - const wildyMultiMultiplier = monster.wildyMulti === true ? 2 : 1; + let wildyMultiMultiplier = monster.wildyMulti || cannonMulti ? 2 : 1; const hasSupplies = supplies ? 0.5 : 1; const hasOverheads = user.skillLevel(SkillsEnum.Prayer) >= 43 ? 0.25 : 1; diff --git a/src/lib/util/repeatStoredTrip.ts b/src/lib/util/repeatStoredTrip.ts index d3d6f63b4a..035129fff4 100644 --- a/src/lib/util/repeatStoredTrip.ts +++ b/src/lib/util/repeatStoredTrip.ts @@ -436,7 +436,8 @@ export const tripHandlers = { return { name: autocompleteMonsters.find(i => i.id === data.monsterID)?.name ?? data.monsterID.toString(), quantity: data.iQty, - method + method, + wilderness: data.isInWilderness }; } }, diff --git a/src/mahoji/commands/activities.ts b/src/mahoji/commands/activities.ts index c164c129e2..1e04e47097 100644 --- a/src/mahoji/commands/activities.ts +++ b/src/mahoji/commands/activities.ts @@ -1,3 +1,4 @@ +import { User } from 'discord.js'; import { ApplicationCommandOptionType, CommandRunOptions } from 'mahoji'; import { @@ -181,7 +182,15 @@ export const activitiesCommand: OSBMahojiCommand = { type: ApplicationCommandOptionType.String, name: 'name', description: 'The name of the quest (optional).', - choices: quests.map(i => ({ name: i.name, value: i.name })), + autocomplete: async (_value: string, user: User) => { + const mUser = await mUserFetch(user.id); + let list = quests + .filter(i => !mUser.user.finished_quest_ids.includes(i.id)) + .map(i => ({ name: i.name, value: i.name })); + if (list.length === 0) + list = quests.map(i => ({ name: `${i.name} (completed)`, value: i.name })); + return list; + }, required: false } ] diff --git a/src/mahoji/commands/create.ts b/src/mahoji/commands/create.ts index 9c1e681a01..8465ba5a38 100644 --- a/src/mahoji/commands/create.ts +++ b/src/mahoji/commands/create.ts @@ -64,7 +64,7 @@ export const createCommand: OSBMahojiCommand = { }: CommandRunOptions<{ item: string; quantity?: number; showall?: boolean }>) => { const user = await mUserFetch(userID.toString()); - const itemName = options.item.toLowerCase(); + const itemName = options.item?.toLowerCase(); let { quantity } = options; if (options.showall) { return allCreatablesTable; diff --git a/src/mahoji/commands/k.ts b/src/mahoji/commands/k.ts index 3099cfae2e..0dbf42544e 100644 --- a/src/mahoji/commands/k.ts +++ b/src/mahoji/commands/k.ts @@ -83,19 +83,21 @@ export const autocompleteMonsters = [ ]; async function fetchUsersRecentlyKilledMonsters(userID: string) { - const res = await prisma.$queryRawUnsafe<{ mon_id: string }[]>( - `SELECT DISTINCT((data->>'monsterID')) AS mon_id + const res = await prisma.$queryRawUnsafe<{ mon_id: string; last_killed: Date }[]>( + `SELECT DISTINCT((data->>'monsterID')) AS mon_id, MAX(start_date) as last_killed FROM activity WHERE user_id = $1 AND type = 'MonsterKilling' AND finish_date > now() - INTERVAL '31 days' +GROUP BY 1 +ORDER BY 2 DESC LIMIT 10;`, BigInt(userID) ); - return new Set(res.map(i => Number(i.mon_id))); + return res.map(i => Number(i.mon_id)); } -export const killCommand: OSBMahojiCommand = { +export const minionKCommand: OSBMahojiCommand = { name: 'k', description: 'Send your minion to kill things.', attributes: { @@ -117,15 +119,17 @@ export const killCommand: OSBMahojiCommand = { : [m.name.toLowerCase(), ...m.aliases].some(str => str.includes(value.toLowerCase())) ) .sort((a, b) => { - const hasA = recentlyKilled.has(a.id); - const hasB = recentlyKilled.has(b.id); - if (hasA && hasB) return 0; + const hasA = recentlyKilled.includes(a.id); + const hasB = recentlyKilled.includes(b.id); + if (hasA && hasB) { + return recentlyKilled.indexOf(a.id) < recentlyKilled.indexOf(b.id) ? -1 : 1; + } if (hasA) return -1; if (hasB) return 1; return 0; }) .map(i => ({ - name: `${i.name}${recentlyKilled.has(i.id) ? ' (Recently killed)' : ''}`, + name: `${i.name}${recentlyKilled.includes(i.id) ? ' (Recently killed)' : ''}`, value: i.name })); } @@ -149,6 +153,18 @@ export const killCommand: OSBMahojiCommand = { name: 'show_info', description: 'Show information on this monster.', required: false + }, + { + type: ApplicationCommandOptionType.Boolean, + name: 'wilderness', + description: 'If you want to kill the monster in the wilderness.', + required: false + }, + { + type: ApplicationCommandOptionType.Boolean, + name: 'solo', + description: 'Solo (if its a group boss)', + required: false } ], run: async ({ @@ -161,11 +177,20 @@ export const killCommand: OSBMahojiCommand = { quantity?: number; method?: PvMMethod; show_info?: boolean; + wilderness?: boolean; }>) => { const user = await mUserFetch(userID); if (options.show_info) { return returnStringOrFile(await monsterInfo(user, options.name)); } - return minionKillCommand(user, interaction, channelID, options.name, options.quantity, options.method); + return minionKillCommand( + user, + interaction, + channelID, + options.name, + options.quantity, + options.method, + options.wilderness + ); } }; diff --git a/src/mahoji/commands/kill.ts b/src/mahoji/commands/kill.ts index e9506acd9e..1fc0b587d1 100644 --- a/src/mahoji/commands/kill.ts +++ b/src/mahoji/commands/kill.ts @@ -98,7 +98,7 @@ export const killCommand: OSBMahojiCommand = { limit, catacombs: false, onTask: false, - lootTableTertiaryChanges: Array.from(user.buildCATertiaryItemChanges().entries()) + lootTableTertiaryChanges: Array.from(user.buildTertiaryItemChanges().entries()) }); if (result.error) { diff --git a/src/mahoji/commands/leaderboard.ts b/src/mahoji/commands/leaderboard.ts index ce813dbbf1..45c1e8f8f1 100644 --- a/src/mahoji/commands/leaderboard.ts +++ b/src/mahoji/commands/leaderboard.ts @@ -416,7 +416,9 @@ async function openLb( name: string, ironmanOnly: boolean ) { - name = name.trim(); + if (name) { + name = name.trim(); + } let entityID = -1; let key = ''; diff --git a/src/mahoji/commands/mine.ts b/src/mahoji/commands/mine.ts index 4cfcf15aa1..8289432689 100644 --- a/src/mahoji/commands/mine.ts +++ b/src/mahoji/commands/mine.ts @@ -8,7 +8,7 @@ import { miningCapeOreEffect, miningGloves, pickaxes, varrockArmours } from '../ import { sinsOfTheFatherSkillRequirements } from '../../lib/skilling/functions/questRequirements'; import Mining from '../../lib/skilling/skills/mining'; import { MiningActivityTaskOptions } from '../../lib/types/minions'; -import { formatDuration, formatSkillRequirements, itemNameFromID, randomVariation } from '../../lib/util'; +import { formatDuration, formatSkillRequirements, itemID, itemNameFromID, randomVariation } from '../../lib/util'; import addSubTaskToActivityTask from '../../lib/util/addSubTaskToActivityTask'; import { calcMaxTripLength } from '../../lib/util/calcMaxTripLength'; import { minionName } from '../../lib/util/minionUtils'; @@ -25,7 +25,8 @@ export function calculateMiningInput({ miningLevel, craftingLevel, strengthLevel, - maxTripLength + maxTripLength, + user }: { nameInput: string; quantityInput: number | undefined; @@ -37,6 +38,7 @@ export function calculateMiningInput({ craftingLevel: number; strengthLevel: number; maxTripLength: number; + user: MUser; }) { const ore = Mining.Ores.find( ore => @@ -105,9 +107,9 @@ export function calculateMiningInput({ let glovesRate = 0; if (miningLevel >= 60) { for (const glove of miningGloves) { - if (!gearValues.some(g => g.hasEquipped(glove.id)) || !glove.Percentages.has(ore.id)) continue; - glovesRate = glove.Percentages.amount(ore.id); - if (glovesRate !== 0) { + if (!user.hasEquipped(glove.id) || !glove.Percentages[ore.name]) continue; + glovesRate = glove.Percentages[ore.name]; + if (glovesRate) { messages.push(`Lowered rock depletion rate by **${glovesRate}%** for ${itemNameFromID(glove.id)}`); break; } @@ -116,9 +118,9 @@ export function calculateMiningInput({ let armourEffect = 0; for (const armour of varrockArmours) { - if (!gearValues.some(g => g.hasEquipped(armour.id)) || !armour.Percentages.has(ore.id)) continue; - armourEffect = armour.Percentages.amount(ore.id); - if (armourEffect !== 0) { + if (!user.hasEquippedOrInBank(armour.id) || !armour.Percentages[ore.name]) continue; + armourEffect = armour.Percentages[ore.name]; + if (armourEffect) { messages.push(`**${armourEffect}%** chance to mine an extra ore using ${itemNameFromID(armour.id)}`); break; } @@ -131,9 +133,9 @@ export function calculateMiningInput({ } let miningCapeEffect = 0; - if (gearValues.some(g => g.hasEquipped('Mining cape')) && miningCapeOreEffect.has(ore.id)) { - miningCapeEffect = miningCapeOreEffect.amount(ore.id); - if (miningCapeEffect !== 0) { + if (user.hasEquippedOrInBank([itemID('Mining cape')]) && miningCapeOreEffect[ore.name]) { + miningCapeEffect = miningCapeOreEffect[ore.name]; + if (miningCapeEffect) { messages.push(`**${miningCapeEffect}%** chance to mine an extra ore using Mining cape`); } } @@ -250,7 +252,8 @@ export const mineCommand: OSBMahojiCommand = { miningLevel: user.skillLevel('mining'), craftingLevel: user.skillLevel('crafting'), strengthLevel: user.skillLevel('strength'), - maxTripLength: calcMaxTripLength(user, 'Mining') + maxTripLength: calcMaxTripLength(user, 'Mining'), + user }); if (typeof result === 'string') { diff --git a/src/mahoji/commands/minion.ts b/src/mahoji/commands/minion.ts index 9e4733a2bb..dc9eeb9a9a 100644 --- a/src/mahoji/commands/minion.ts +++ b/src/mahoji/commands/minion.ts @@ -29,7 +29,7 @@ import creatures from '../../lib/skilling/skills/hunter/creatures'; import { MUserStats } from '../../lib/structures/MUserStats'; import { convertLVLtoXP, getAllIDsOfUser, getUsername, isValidNickname } from '../../lib/util'; import { getKCByName } from '../../lib/util/getKCByName'; -import getOSItem from '../../lib/util/getOSItem'; +import getOSItem, { getItem } from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { minionStatsEmbed } from '../../lib/util/minionStatsEmbed'; import { checkPeakTimes } from '../../lib/util/minionUtils'; @@ -238,7 +238,8 @@ export const minionCommand: OSBMahojiCommand = { autocomplete: async (value, user) => { const mappedLampables = Lampables.map(i => i.items) .flat(2) - .map(getOSItem) + .map(getItem) + .filter(notEmpty) .map(i => ({ id: i.id, name: i.name })); const botUser = await mUserFetch(user.id); diff --git a/src/mahoji/commands/offer.ts b/src/mahoji/commands/offer.ts index c64172ef90..1a2f5a83ad 100644 --- a/src/mahoji/commands/offer.ts +++ b/src/mahoji/commands/offer.ts @@ -54,7 +54,7 @@ function notifyUniques(user: MUser, activity: string, uniques: number[], loot: B } } -export const mineCommand: OSBMahojiCommand = { +export const offerCommand: OSBMahojiCommand = { name: 'offer', description: 'Offer bones or bird eggs.', attributes: { diff --git a/src/mahoji/commands/rates.ts b/src/mahoji/commands/rates.ts index a9ccf0a02c..ae22d051c3 100644 --- a/src/mahoji/commands/rates.ts +++ b/src/mahoji/commands/rates.ts @@ -653,7 +653,8 @@ ${zygomiteFarmingSource miningLevel, craftingLevel: 120, strengthLevel: 120, - maxTripLength: duration + maxTripLength: duration, + user }); if (typeof result === 'string') continue; const spiritOre = stoneSpirits.find(t => t.ore.id === ore.id); diff --git a/src/mahoji/commands/testpotato.ts b/src/mahoji/commands/testpotato.ts index f5df08df58..c76dd6648e 100644 --- a/src/mahoji/commands/testpotato.ts +++ b/src/mahoji/commands/testpotato.ts @@ -81,6 +81,12 @@ export async function giveMaxStats(user: MUser) { } await user.update({ QP: MAX_QP, + slayer_points: 50_000, + nmz_points: 50_000, + volcanic_mine_points: 500_000, + carpenter_points: 5_000_000, + zeal_tokens: 500_000, + lms_points: 500_000, ...updates }); } diff --git a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts index 2d81c2e2cb..1e59cb90de 100644 --- a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts +++ b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts @@ -202,6 +202,171 @@ const AutoSlayMaxEfficiencyTable: AutoslayLink[] = [ efficientName: Monsters.Lizardman.name, efficientMonster: Monsters.Lizardman.id, efficientMethod: 'cannon' + }, + { + monsterID: Monsters.RevenantImp.id, + efficientName: Monsters.RevenantDemon.name, + efficientMonster: Monsters.RevenantDemon.id, + efficientMethod: 'none' + } +]; + +const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ + { + monsterID: Monsters.AbyssalDemon.id, + efficientName: Monsters.AbyssalDemon.name, + efficientMonster: Monsters.AbyssalDemon.id, + efficientMethod: 'barrage' + }, + { + monsterID: Monsters.Ankou.id, + efficientName: Monsters.Ankou.name, + efficientMonster: Monsters.Ankou.id, + efficientMethod: 'barrage' + }, + { + monsterID: Monsters.BlackDemon.id, + efficientName: Monsters.BlackDemon.name, + efficientMonster: Monsters.BlackDemon.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.BlackKnight.id, + efficientName: Monsters.BlackKnight.name, + efficientMonster: Monsters.BlackKnight.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.Bloodveld.id, + efficientName: Monsters.Bloodveld.name, + efficientMonster: Monsters.Bloodveld.id, + efficientMethod: 'barrage' + }, + { + monsterID: Monsters.ChaosDruid.id, + efficientName: Monsters.ChaosDruid.name, + efficientMonster: Monsters.ChaosDruid.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.DarkWarrior.id, + efficientName: Monsters.DarkWarrior.name, + efficientMonster: Monsters.DarkWarrior.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.DeadlyRedSpider.id, + efficientName: Monsters.DeadlyRedSpider.name, + efficientMonster: Monsters.DeadlyRedSpider.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.DustDevil.id, + efficientName: Monsters.DustDevil.name, + efficientMonster: Monsters.DustDevil.id, + efficientMethod: 'barrage' + }, + { + monsterID: Monsters.ElderChaosDruid.id, + efficientName: Monsters.ElderChaosDruid.name, + efficientMonster: Monsters.ElderChaosDruid.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.Ent.id, + efficientName: Monsters.Ent.name, + efficientMonster: Monsters.Ent.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.GreaterDemon.id, + efficientName: Monsters.GreaterDemon.name, + efficientMonster: Monsters.GreaterDemon.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.GreenDragon.id, + efficientName: Monsters.GreenDragon.name, + efficientMonster: Monsters.GreenDragon.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.GuardBandit.id, + efficientName: Monsters.GuardBandit.name, + efficientMonster: Monsters.GuardBandit.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.Hellhound.id, + efficientName: Monsters.Hellhound.name, + efficientMonster: Monsters.Hellhound.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.IceGiant.id, + efficientName: Monsters.IceGiant.name, + efficientMonster: Monsters.IceGiant.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.IceWarrior.id, + efficientName: Monsters.IceWarrior.name, + efficientMonster: Monsters.IceWarrior.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.Jelly.id, + efficientName: Monsters.Jelly.name, + efficientMonster: Monsters.Jelly.id, + efficientMethod: 'barrage' + }, + { + monsterID: Monsters.LesserDemon.id, + efficientName: Monsters.LesserDemon.name, + efficientMonster: Monsters.LesserDemon.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.MagicAxe.id, + efficientName: Monsters.MagicAxe.name, + efficientMonster: Monsters.MagicAxe.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.Mammoth.id, + efficientName: Monsters.Mammoth.name, + efficientMonster: Monsters.Mammoth.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.MossGiant.id, + efficientName: Monsters.MossGiant.name, + efficientMonster: Monsters.MossGiant.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.Nechryael.id, + efficientName: Monsters.Nechryael.name, + efficientMonster: Monsters.Nechryael.id, + efficientMethod: 'barrage' + }, + { + monsterID: Monsters.RevenantImp.id, + efficientName: Monsters.RevenantDemon.name, + efficientMonster: Monsters.RevenantDemon.id, + efficientMethod: 'none' + }, + { + monsterID: Monsters.Scorpion.id, + efficientName: Monsters.Scorpion.name, + efficientMonster: Monsters.Scorpion.id, + efficientMethod: 'cannon' + }, + { + monsterID: Monsters.Spider.id, + efficientName: Monsters.Spider.name, + efficientMonster: Monsters.Spider.id, + efficientMethod: 'cannon' } ]; @@ -285,11 +450,19 @@ export async function autoSlayCommand({ return; } if (method === 'ehp') { - const ehpMonster = AutoSlayMaxEfficiencyTable.find(e => { + let ehpMonster = AutoSlayMaxEfficiencyTable.find(e => { const masterMatch = !e.slayerMasters || e.slayerMasters.includes(usersTask.currentTask!.slayer_master_id); return masterMatch && e.monsterID === usersTask.assignedTask!.monster.id; }); + if (usersTask.currentTask.slayer_master_id === 8) { + ehpMonster = WildyAutoSlayMaxEfficiencyTable.find(e => { + const masterMatch = + !e.slayerMasters || e.slayerMasters.includes(usersTask.currentTask!.slayer_master_id); + return masterMatch && e.monsterID === usersTask.assignedTask!.monster.id; + }); + } + const ehpKillable = killableMonsters.find(m => m.id === ehpMonster?.efficientMonster); // If we don't have the requirements for the efficient monster, revert to default monster diff --git a/src/mahoji/lib/abstracted_commands/barbAssault.ts b/src/mahoji/lib/abstracted_commands/barbAssault.ts index 2efd744b84..8a9ad1fb43 100644 --- a/src/mahoji/lib/abstracted_commands/barbAssault.ts +++ b/src/mahoji/lib/abstracted_commands/barbAssault.ts @@ -171,7 +171,7 @@ export async function barbAssaultGambleCommand( interaction: ChatInputCommandInteraction, user: MUser, tier: string, - quantity: number + quantity = 1 ) { const buyable = GambleTiers.find(i => stringMatches(tier, i.name)); if (!buyable) { diff --git a/src/mahoji/lib/abstracted_commands/minionKill.ts b/src/mahoji/lib/abstracted_commands/minionKill.ts index 2020f6e70f..6e365538dc 100644 --- a/src/mahoji/lib/abstracted_commands/minionKill.ts +++ b/src/mahoji/lib/abstracted_commands/minionKill.ts @@ -111,6 +111,12 @@ const revSpecialWeapons = { mage: getOSItem("Thammaron's sceptre") } as const; +const revUpgradedWeapons = { + melee: getOSItem('Ursine chainmace'), + range: getOSItem('Webweaver bow'), + mage: getOSItem('Accursed sceptre') +} as const; + function formatMissingItems(consumables: Consumable[], timeToFinish: number) { const str = []; @@ -162,7 +168,8 @@ export async function minionKillCommand( channelID: string, name: string, quantity: number | undefined, - method: PvMMethod | undefined + method: PvMMethod | undefined, + wilderness: boolean | undefined ) { if (user.minionIsBusy) { return 'Your minion is busy.'; @@ -223,6 +230,16 @@ export async function minionKillCommand( return `You can't kill ${monster.name}, because you're not on a slayer task.`; } + if (monster.canBePked && wilderness === false) { + return `You can't kill ${monster.name} outside the wilderness.`; + } + + const isInWilderness = wilderness || (isOnTask && usersTask.assignedTask?.wilderness) || monster.canBePked; + + if (!monster.wildy && isInWilderness) { + return `You can't kill ${monster.name} in the wilderness.`; + } + const wildyGearStat = wildyGear.getStats()[key]; const revGearPercent = Math.max(0, calcWhatPercent(wildyGearStat, maxOffenceStats[key])); @@ -237,6 +254,12 @@ export async function minionKillCommand( } } + // Add jelly & bloodveld check as can barrage in wilderness + const jelly = monster.id === Monsters.Jelly.id; + const bloodveld = monster.id === Monsters.Bloodveld.id; + + const wildyBurst = (jelly || bloodveld) && isInWilderness; + // Set chosen boost based on priority: const myCBOpts = user.combatOptions; const boostChoice = determineBoostChoice({ @@ -244,7 +267,8 @@ export async function minionKillCommand( user, monster, method, - isOnTask + isOnTask, + wildyBurst }); // Check requirements @@ -314,7 +338,7 @@ export async function minionKillCommand( } } - for (const [itemID, boostAmount] of Object.entries(resolveAvailableItemBoosts(user, monster))) { + for (const [itemID, boostAmount] of Object.entries(resolveAvailableItemBoosts(user, monster, isInWilderness))) { timeToFinish *= (100 - boostAmount) / 100; boosts.push(`${boostAmount}% for ${itemNameFromID(parseInt(itemID))}`); } @@ -323,11 +347,11 @@ export async function minionKillCommand( timeToFinish = reduceNumByPercent(timeToFinish, 20); boosts.push('20% boost for Gregoyle'); } - if (user.hasEquipped('Dwarven warhammer') && !monster.wildy) { + if (user.hasEquipped('Dwarven warhammer') && !isInWilderness) { timeToFinish = reduceNumByPercent(timeToFinish, 40); boosts.push('40% boost for Dwarven warhammer'); } - if (user.gear.wildy.hasEquipped(['Hellfire bow']) && monster.wildy) { + if (user.gear.wildy.hasEquipped(['Hellfire bow']) && isInWilderness) { timeToFinish /= 3; boosts.push('3x boost for Hellfire bow'); } @@ -343,16 +367,37 @@ export async function minionKillCommand( let virtusBoost = 0; let virtusBoostMsg = ''; - const dragonBoost = 15; // Common boost percentage for dragon-related gear + let dragonBoost = 0; + let dragonBoostMsg = ''; + let revBoost = 0; + let revBoostMsg = ''; const isUndead = osjsMon?.data?.attributes?.includes(MonsterAttribute.Undead); const isDragon = osjsMon?.data?.attributes?.includes(MonsterAttribute.Dragon); + function applyRevWeaponBoost() { + const style = convertAttackStylesToSetup(user.user.attack_style); + const specialWeapon = revSpecialWeapons[style]; + const upgradedWeapon = revUpgradedWeapons[style]; + + if (wildyGear.hasEquipped(specialWeapon.name)) { + revBoost = 12.5; + timeToFinish = reduceNumByPercent(timeToFinish, revBoost); + revBoostMsg = `${revBoost}% for ${specialWeapon.name}`; + } + + if (wildyGear.hasEquipped(upgradedWeapon.name)) { + revBoost = 17.5; + timeToFinish = reduceNumByPercent(timeToFinish, revBoost); + revBoostMsg = `${revBoost}% for ${upgradedWeapon.name}`; + } + } + function applyDragonBoost() { - const hasDragonLance = monster?.canBePked + const hasDragonLance = isInWilderness ? wildyGear.hasEquipped('Dragon hunter lance') : user.hasEquippedOrInBank('Dragon hunter lance'); - const hasDragonCrossbow = monster?.canBePked + const hasDragonCrossbow = isInWilderness ? wildyGear.hasEquipped('Dragon hunter crossbow') : user.hasEquippedOrInBank('Dragon hunter crossbow'); @@ -360,25 +405,25 @@ export async function minionKillCommand( (hasDragonLance && !attackStyles.includes(SkillsEnum.Ranged) && !attackStyles.includes(SkillsEnum.Magic)) || (hasDragonCrossbow && attackStyles.includes(SkillsEnum.Ranged)) ) { - const boostMessage = hasDragonLance + dragonBoost = 15; // Common boost percentage for dragon-related gear + dragonBoostMsg = hasDragonLance ? `${dragonBoost}% for Dragon hunter lance` : `${dragonBoost}% for Dragon hunter crossbow`; timeToFinish = reduceNumByPercent(timeToFinish, dragonBoost); - boosts.push(boostMessage); } } function applyBlackMaskBoost() { - const hasInfernalSlayerHelmI = monster?.canBePked + const hasInfernalSlayerHelmI = isInWilderness ? wildyGear.hasEquipped('Infernal slayer helmet(i)') : user.hasEquippedOrInBank('Infernal slayer helmet(i)'); - const hasInfernalSlayerHelm = monster?.canBePked + const hasInfernalSlayerHelm = isInWilderness ? wildyGear.hasEquipped('Infernal slayer helmet') : user.hasEquippedOrInBank('Infernal slayer helmet'); - const hasBlackMask = monster?.canBePked + const hasBlackMask = isInWilderness ? wildyGear.hasEquipped('Black mask') : user.hasEquippedOrInBank('Black mask'); - const hasBlackMaskI = monster?.canBePked + const hasBlackMaskI = isInWilderness ? wildyGear.hasEquipped('Black mask (i)') : user.hasEquippedOrInBank('Black mask (i)'); @@ -406,10 +451,10 @@ export async function minionKillCommand( let salveEnhanced = false; const style = attackStyles[0]; if (style === 'ranged' || style === 'magic') { - salveBoost = monster?.canBePked + salveBoost = isInWilderness ? wildyGear.hasEquipped('Salve amulet(i)') : user.hasEquippedOrInBank('Salve amulet (i)'); - salveEnhanced = monster?.canBePked + salveEnhanced = isInWilderness ? wildyGear.hasEquipped('Salve amulet(ei)') : user.hasEquippedOrInBank('Salve amulet (ei)'); if (salveBoost) { @@ -419,10 +464,10 @@ export async function minionKillCommand( } on non-melee task`; } } else { - salveBoost = monster?.canBePked + salveBoost = isInWilderness ? wildyGear.hasEquipped('Salve amulet') : user.hasEquippedOrInBank('Salve amulet'); - salveEnhanced = monster?.canBePked + salveEnhanced = isInWilderness ? wildyGear.hasEquipped('Salve amulet (e)') : user.hasEquippedOrInBank('Salve amulet (e)'); if (salveBoost) { @@ -434,10 +479,19 @@ export async function minionKillCommand( } } + if (isInWilderness && monster.revsWeaponBoost) { + applyRevWeaponBoost(); + } + function calculateVirtusBoost() { let virtusPiecesEquipped = 0; + for (const item of resolveItems(['Virtus mask', 'Virtus robe top', 'Virtus robe legs'])) { - if (user.gear.mage.hasEquipped(item)) { + if (isInWilderness) { + if (wildyGear.hasEquipped(item)) { + virtusPiecesEquipped += blackMaskBoost !== 0 && itemNameFromID(item) === 'Virtus mask' ? 0 : 1; + } + } else if (user.gear.mage.hasEquipped(item)) { virtusPiecesEquipped += blackMaskBoost !== 0 && itemNameFromID(item) === 'Virtus mask' ? 0 : 1; } } @@ -446,7 +500,7 @@ export async function minionKillCommand( virtusBoostMsg = virtusPiecesEquipped > 1 ? ` with ${virtusPiecesEquipped} Virtus pieces` - : virtusPiecesEquipped > 0 + : virtusPiecesEquipped === 1 ? ` with ${virtusPiecesEquipped} Virtus piece` : ''; } @@ -474,6 +528,17 @@ export async function minionKillCommand( } } + // Only choose greater boost: + if (dragonBoost || revBoost) { + if (revBoost > dragonBoost) { + timeToFinish = reduceNumByPercent(timeToFinish, revBoost); + boosts.push(revBoostMsg); + } else { + timeToFinish = reduceNumByPercent(timeToFinish, dragonBoost); + boosts.push(dragonBoostMsg); + } + } + if (revenants) { timeToFinish = reduceNumByPercent(timeToFinish, revGearPercent / 4); boosts.push(`${(revGearPercent / 4).toFixed(2)}% (out of a possible 25%) for ${key}`); @@ -499,15 +564,14 @@ export async function minionKillCommand( if (!isOnTask && method && method !== 'none') { return 'You can only burst/barrage/cannon while on task in BSO.'; } - if ((method === 'burst' || method === 'barrage') && !monster!.canBarrage) { + if (!wildyBurst && (method === 'burst' || method === 'barrage') && !monster!.canBarrage) { return `${monster!.name} cannot be barraged or burst.`; } if (method === 'cannon' && !hasCannon) { return "You don't own a Dwarf multicannon, so how could you use one?"; } - if (method === 'cannon' && !monster!.canCannon) { - return `${monster!.name} cannot be killed with a cannon.`; - } + + // Check for stats if (boostChoice === 'barrage' && user.skillLevel(SkillsEnum.Magic) < 94) { return `You need 94 Magic to use Ice Barrage. You have ${user.skillLevel(SkillsEnum.Magic)}`; } @@ -523,6 +587,35 @@ export async function minionKillCommand( return `You need 65 Ranged to use Chinning method. You have ${user.skillLevel(SkillsEnum.Ranged)}`; } + // Wildy Monster checks + if (isInWilderness === true && boostChoice === 'cannon') { + if (monster.id === Monsters.HillGiant.id || monster.id === Monsters.MossGiant.id) { + usingCannon = isInWilderness; + } + if (monster.wildySlayerCave) { + usingCannon = isInWilderness; + cannonMulti = isInWilderness; + if (monster.id === Monsters.AbyssalDemon.id && !isOnTask) { + usingCannon = false; + cannonMulti = false; + } + } + } + + if ((method === 'burst' || method === 'barrage') && !monster!.canBarrage) { + if (jelly || bloodveld) { + if (!isInWilderness) { + return `${monster.name} can only be barraged or burst in the wilderness.`; + } + } else return `${monster!.name} cannot be barraged or burst.`; + } + + if (!usingCannon) { + if (method === 'cannon' && !monster!.canCannon) { + return `${monster!.name} cannot be killed with a cannon.`; + } + } + if ( boostChoice === 'cannon' && !user.user.disabled_inventions.includes(InventionID.SuperiorDwarfMultiCannon) && @@ -542,25 +635,33 @@ export async function minionKillCommand( timeToFinish = reduceNumByPercent(timeToFinish, boost); boosts.push(`${boost}% for Superior Cannon (${res.messages})`); } - } else if (boostChoice === 'barrage' && attackStyles.includes(SkillsEnum.Magic) && monster!.canBarrage) { + } else if ( + boostChoice === 'barrage' && + attackStyles.includes(SkillsEnum.Magic) && + (monster!.canBarrage || wildyBurst) + ) { consumableCosts.push(iceBarrageConsumables); calculateVirtusBoost(); timeToFinish = reduceNumByPercent(timeToFinish, boostIceBarrage + virtusBoost); boosts.push(`${boostIceBarrage + virtusBoost}% for Ice Barrage${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBarrage; - } else if (boostChoice === 'burst' && attackStyles.includes(SkillsEnum.Magic) && monster!.canBarrage) { + } else if ( + boostChoice === 'burst' && + attackStyles.includes(SkillsEnum.Magic) && + (monster!.canBarrage || wildyBurst) + ) { consumableCosts.push(iceBurstConsumables); calculateVirtusBoost(); timeToFinish = reduceNumByPercent(timeToFinish, boostIceBurst + virtusBoost); boosts.push(`${boostIceBurst + virtusBoost}% for Ice Burst${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBurst; - } else if (boostChoice === 'cannon' && hasCannon && monster!.cannonMulti) { + } else if ((boostChoice === 'cannon' && hasCannon && monster!.cannonMulti) || cannonMulti) { usingCannon = true; cannonMulti = true; consumableCosts.push(cannonMultiConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannonMulti); boosts.push(`${boostCannonMulti}% for Cannon in multi`); - } else if (boostChoice === 'cannon' && hasCannon && monster!.canCannon) { + } else if ((boostChoice === 'cannon' && hasCannon && monster!.canCannon) || usingCannon) { usingCannon = true; consumableCosts.push(cannonSingleConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannon); @@ -601,7 +702,7 @@ export async function minionKillCommand( timeToFinish *= 0.8; boosts.push('20% for Dwarven blessing'); } - if (monster.wildy && hasZealotsAmulet) { + if (isInWilderness && hasZealotsAmulet) { timeToFinish *= 0.95; boosts.push('5% for Amulet of zealots'); } @@ -660,9 +761,13 @@ export async function minionKillCommand( for (const degItem of degradeablePvmBoostItems) { const isUsing = convertPvmStylesToGearSetup(attackStyles).includes(degItem.attackStyle) && - user.gear[degItem.attackStyle].hasEquipped(degItem.item.id) && (monster.setupsUsed ? monster.setupsUsed.includes(degItem.attackStyle) : true); - if (isUsing) { + + const gearCheck = isInWilderness + ? user.gear.wildy.hasEquipped(degItem.item.id) + : user.gear[degItem.attackStyle].hasEquipped(degItem.item.id); + + if (isUsing && gearCheck) { // We assume they have enough charges, add the boost, and degrade at the end to avoid doing it twice. degItemBeingUsed.push(degItem); } @@ -965,7 +1070,7 @@ export async function minionKillCommand( let hasDied: boolean | undefined = undefined; let hasWildySupplies = undefined; - if (monster.canBePked) { + if (isInWilderness) { await increaseWildEvasionXp(user, duration); thePkCount = 0; hasDied = false; @@ -1022,7 +1127,8 @@ export async function minionKillCommand( wildyPeak!, monster, duration, - hasWildySupplies + hasWildySupplies, + cannonMulti ); thePkCount = pkCount; hasDied = died; @@ -1038,7 +1144,7 @@ export async function minionKillCommand( foodStr += foodMessages; let gearToCheck: GearSetupType = convertAttackStyleToGearSetup(monster.attackStyleToUse); - if (monster.wildy) gearToCheck = 'wildy'; + if (isInWilderness) gearToCheck = 'wildy'; try { const { foodRemoved, reductions, reductionRatio } = await removeFoodFromUser({ @@ -1046,11 +1152,11 @@ export async function minionKillCommand( totalHealingNeeded: healAmountNeeded * quantity, healPerAction: Math.ceil(healAmountNeeded / quantity), activityName: monster.name, - attackStylesUsed: monster.wildy + attackStylesUsed: isInWilderness ? ['wildy'] : uniqueArr([...objectKeys(monster.minimumGearRequirements ?? {}), gearToCheck]), learningPercentage: percentReduced, - isWilderness: monster.wildy, + isWilderness: isInWilderness, minimumHealAmount: monster.minimumFoodHealAmount }); @@ -1106,7 +1212,7 @@ export async function minionKillCommand( // Remove items after food calc to prevent losing items if the user doesn't have the right amount of food. Example: Mossy key if (lootToRemove.length > 0) { updateBankSetting('economyStats_PVMCost', lootToRemove); - await user.specialRemoveItems(lootToRemove, { wildy: monster.wildy ? true : false }); + await user.specialRemoveItems(lootToRemove, { wildy: isInWilderness ? true : false }); totalCost.add(lootToRemove); } @@ -1139,7 +1245,8 @@ export async function minionKillCommand( burstOrBarrage: !burstOrBarrage ? undefined : burstOrBarrage, died: hasDied, pkEncounters: thePkCount, - hasWildySupplies + hasWildySupplies, + isInWilderness }); if (usedDart) { diff --git a/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts b/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts index 4cca01aea2..62a214583a 100644 --- a/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts +++ b/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts @@ -99,7 +99,8 @@ export async function slayerListBlocksCommand(mahojiUser: MUser) { export async function slayerStatusCommand(mahojiUser: MUser) { const { currentTask, assignedTask, slayerMaster } = await getUsersCurrentSlayerInfo(mahojiUser.id); const { slayer_points: slayerPoints } = mahojiUser.user; - const { slayer_task_streak: slayerStreak } = await mahojiUser.fetchStats({ slayer_task_streak: true }); + const slayer_streaks = await mahojiUser.fetchStats({ slayer_task_streak: true, slayer_wildy_task_streak: true }); + return ( `${ currentTask @@ -110,7 +111,9 @@ export async function slayerStatusCommand(mahojiUser: MUser) { )}. You have ${currentTask.quantity_remaining.toLocaleString()} kills remaining.` : '' }` + - `\nYou have ${slayerPoints.toLocaleString()} slayer points, and have completed ${slayerStreak} tasks in a row.` + `\nYou have ${slayerPoints.toLocaleString()} slayer points, and have completed ${ + slayer_streaks.slayer_task_streak + } tasks in a row and ${slayer_streaks.slayer_wildy_task_streak} wilderness tasks in a row.` ); } @@ -241,8 +244,9 @@ export async function slayerNewTaskCommand({ const has99SlayerCape = user.skillLevel('slayer') >= 99 && user.hasEquippedOrInBank('Slayer cape'); - // Chooses a default slayer master: + // Chooses a default slayer master (excluding Krystilia): const proposedDefaultMaster = slayerMasters + .filter(sm => sm.id !== 8) // Exclude Krystilia .sort((a, b) => b.basePoints - a.basePoints) .find(sm => userCanUseMaster(user, sm)); @@ -276,11 +280,13 @@ export async function slayerNewTaskCommand({ interactionReply(interaction, 'You cannot skip this task because Turael assigns it.'); return; } + const isUsingKrystilia = Boolean(currentTask?.slayer_master_id === 8); + const taskStreakKey = isUsingKrystilia ? 'slayer_wildy_task_streak' : 'slayer_task_streak'; + const warning = `Really cancel task? This will reset your${ + isUsingKrystilia ? ' wilderness' : '' + } streak to 0 and give you a new ${slayerMaster.name} task.`; - await handleMahojiConfirmation( - interaction, - `Really cancel task? This will reset your streak to 0 and give you a new ${slayerMaster.name} task.` - ); + await handleMahojiConfirmation(interaction, warning); await prisma.slayerTask.update({ where: { id: currentTask.id @@ -290,7 +296,8 @@ export async function slayerNewTaskCommand({ quantity_remaining: 0 } }); - await userStatsUpdate(user.id, { slayer_task_streak: 0 }, {}); + await userStatsUpdate(user.id, { [taskStreakKey]: 0 }, {}); + const newSlayerTask = await assignNewSlayerTask(user, slayerMaster); let commonName = getCommonTaskName(newSlayerTask.assignedTask!.monster); const returnMessage = diff --git a/src/mahoji/mahojiSettings.ts b/src/mahoji/mahojiSettings.ts index a9f1f44ec7..01a32f60f9 100644 --- a/src/mahoji/mahojiSettings.ts +++ b/src/mahoji/mahojiSettings.ts @@ -310,7 +310,7 @@ export function hasMonsterRequirements(user: MUser, monster: KillableMonster) { return [true]; } -export function resolveAvailableItemBoosts(user: MUser, monster: KillableMonster) { +export function resolveAvailableItemBoosts(user: MUser, monster: KillableMonster, isInWilderness: boolean = false) { const boosts = new Bank(); if (monster.itemInBankBoosts) { for (const boostSet of monster.itemInBankBoosts) { @@ -320,7 +320,7 @@ export function resolveAvailableItemBoosts(user: MUser, monster: KillableMonster // find the highest boost that the player has for (const [itemID, boostAmount] of Object.entries(boostSet)) { const parsedId = parseInt(itemID); - if (!user.hasEquippedOrInBank(parsedId)) { + if (isInWilderness ? !user.hasEquipped(parsedId) : !user.hasEquippedOrInBank(parsedId)) { continue; } if (boostAmount > highestBoostAmount) { diff --git a/src/tasks/minions/monsterActivity.ts b/src/tasks/minions/monsterActivity.ts index 6f5dd0be3d..5bb04dd89d 100644 --- a/src/tasks/minions/monsterActivity.ts +++ b/src/tasks/minions/monsterActivity.ts @@ -175,7 +175,8 @@ export const monsterTask: MinionTask = { burstOrBarrage, died, pkEncounters, - hasWildySupplies + hasWildySupplies, + isInWilderness } = data; const monster = killableMonsters.find(mon => mon.id === monsterID)!; @@ -369,7 +370,12 @@ export const monsterTask: MinionTask = { const isOnTaskResult = await isOnSlayerTask({ user, monsterID, quantityKilled: quantity }); const superiorTable = isOnTaskResult.hasSuperiorsUnlocked && monster.superior ? monster.superior : undefined; - const isInCatacombs = !usingCannon ? monster.existsInCatacombs ?? undefined : undefined; + const isInCatacombs = (!usingCannon ? monster.existsInCatacombs ?? undefined : undefined) && !isInWilderness; + + let hasRingOfWealthI = user.gear.wildy.hasEquipped('Ring of wealth (i)') && isInWilderness; + if (hasRingOfWealthI) { + messages.push('\nYour clue scroll chance is doubled due to wearing a Ring of Wealth (i).'); + } const killOptions: MonsterKillOptions = { onSlayerTask: isOnTaskResult.isOnTask, @@ -377,7 +383,11 @@ export const monsterTask: MinionTask = { hasSuperiors: superiorTable, inCatacombs: isInCatacombs, lootTableOptions: { - tertiaryItemPercentageChanges: user.buildCATertiaryItemChanges() + tertiaryItemPercentageChanges: user.buildTertiaryItemChanges( + hasRingOfWealthI, + isInWilderness, + isOnTaskResult.isOnTask + ) } }; @@ -387,14 +397,24 @@ export const monsterTask: MinionTask = { // Calculate superiors and assign loot. let newSuperiorCount = 0; + if (superiorTable && isOnTaskResult.isOnTask) { - let superiorDroprate = 200; - if (user.hasCompletedCATier('elite')) { - superiorDroprate = 150; - messages.push(`${Emoji.CombatAchievements} 25% more common superiors due to Elite CA tier`); - } - for (let i = 0; i < quantity; i++) { - if (roll(superiorDroprate)) newSuperiorCount++; + if (!(isInWilderness && monster.name === 'Bloodveld')) { + let superiorDroprate = 200; + if (isInWilderness) { + superiorDroprate *= 0.9; + messages.push('\n10% more common superiors due to Wilderness Slayer.'); + } + if (user.hasCompletedCATier('elite')) { + superiorDroprate *= 0.75; + messages.push(`\n${Emoji.CombatAchievements} 25% more common superiors due to Elite CA tier.`); + } + + for (let i = 0; i < quantity; i++) { + if (roll(superiorDroprate)) { + newSuperiorCount++; + } + } } } @@ -410,6 +430,16 @@ export const monsterTask: MinionTask = { // Superior loot and totems if in catacombs loot.add(superiorTable!.kill(newSuperiorCount)); if (isInCatacombs) loot.add('Dark totem base', newSuperiorCount); + if (isInWilderness) loot.add("Larran's key", newSuperiorCount); + } + + // Hill giant key wildy buff + if (isInWilderness && monster.name === 'Hill giant') { + for (let i = 0; i < quantity; i++) { + if (roll(128)) { + loot.add('Giant key'); + } + } } const xpRes: string[] = []; @@ -583,9 +613,32 @@ export const monsterTask: MinionTask = { } const quantityLeft = Math.max(0, isOnTaskResult.currentTask!.quantity_remaining - effectiveSlayed); + const isUsingKrystilia = isOnTaskResult.slayerMaster.id === 8; thisTripFinishesTask = quantityLeft === 0; - if (thisTripFinishesTask) { + if (thisTripFinishesTask && isUsingKrystilia) { + const newStats = await userStatsUpdate( + user.id, + { + slayer_wildy_task_streak: { + increment: 1 + } + }, + { slayer_wildy_task_streak: true } + ); + const currentStreak = newStats.slayer_wildy_task_streak; + const points = await calculateSlayerPoints(currentStreak, isOnTaskResult.slayerMaster, user); + const secondNewUser = await user.update({ + slayer_points: { + increment: points + } + }); + str += `\n**You've completed ${currentStreak} wilderness tasks and received ${points} points; giving you a total of ${secondNewUser.newUser.slayer_points}; return to a Slayer master.**`; + if (isOnTaskResult.assignedTask.isBoss) { + str += ` ${await user.addXP({ skillName: SkillsEnum.Slayer, amount: 5000, minimal: true })}`; + str += ' for completing your boss task.'; + } + } else if (thisTripFinishesTask) { const newStats = await userStatsUpdate( user.id, { diff --git a/src/tasks/minions/pickpocketActivity.ts b/src/tasks/minions/pickpocketActivity.ts index 6e59db77ea..ff95791c08 100644 --- a/src/tasks/minions/pickpocketActivity.ts +++ b/src/tasks/minions/pickpocketActivity.ts @@ -77,7 +77,7 @@ export const pickpocketTask: MinionTask = { if (obj.type === 'pickpockable') { for (let i = 0; i < successfulQuantity; i++) { const lootItems = obj.table.roll(1, { - tertiaryItemPercentageChanges: user.buildCATertiaryItemChanges() + tertiaryItemPercentageChanges: user.buildTertiaryItemChanges() }); // TODO: Remove Rocky from loot tables in oldschoolJS if (lootItems.has('Rocky')) lootItems.remove('Rocky'); diff --git a/tests/globalSetup.ts b/tests/globalSetup.ts index 72c831572d..05250a3389 100644 --- a/tests/globalSetup.ts +++ b/tests/globalSetup.ts @@ -5,7 +5,15 @@ import { Collection } from 'discord.js'; import { vi } from 'vitest'; vi.mock('@oldschoolgg/toolkit', async () => { - const actualToolkit = await vi.importActual('@oldschoolgg/toolkit'); // Import all actual exports + const actual: any = await vi.importActual('@oldschoolgg/toolkit'); + return { + ...actual, + mentionCommand: async (_args: any) => 'hi' + }; +}); + +vi.mock('../node_modules/@oldschoolgg/toolkit/src/util/discord.ts', async () => { + const actualToolkit = await vi.importActual('../node_modules/@oldschoolgg/toolkit/src/util/discord.ts'); // Import all actual exports return { ...actualToolkit, // Include all actual exports in the mock mentionCommand: vi.fn().mockReturnValue('') // Mock mentionCommand to return a blank string @@ -18,21 +26,19 @@ global.globalClient = { guilds: { cache: new Collection() }, mahojiClient: { commands: { - values: [ - { - name: 'test', - description: 'test description', - attributes: { description: 'test description' }, - options: [] - } - ] + values: ['test'].map(n => ({ + name: n, + description: 'test description', + attributes: { description: 'test description' }, + options: [{ name: 'claim' }] + })) } }, users: { cache: new Collection() }, channels: { - cache: new Collection() + cache: new Collection().set('1', { id: '1' }) }, busyCounterCache: new Map() } as any; diff --git a/tests/integration/allCommandsBase.test.ts b/tests/integration/allCommandsBase.test.ts index 59800bd64a..f543de60d7 100644 --- a/tests/integration/allCommandsBase.test.ts +++ b/tests/integration/allCommandsBase.test.ts @@ -1,98 +1,326 @@ -import { describe, test, vi } from 'vitest'; +import { join } from 'node:path'; +import { ApplicationCommandOptionType } from 'discord.js'; +import { randArrItem, randInt, shuffleArr, Time } from 'e'; +import { Store } from 'mahoji/dist/lib/structures/Store'; +import { CommandOption } from 'mahoji/dist/lib/types'; +import { isValidCommand } from 'mahoji/dist/lib/util'; +import { Bank, Items } from 'oldschooljs'; +import { expect, test, vi } from 'vitest'; + +import { BitField, minionActivityCache } from '../../src/lib/constants'; +import { prisma } from '../../src/lib/settings/prisma'; +import { mahojiClientSettingsFetch } from '../../src/lib/util/clientSettings'; +import { handleMahojiConfirmation } from '../../src/lib/util/handleMahojiConfirmation'; import { activitiesCommand } from '../../src/mahoji/commands/activities'; +import { adminCommand } from '../../src/mahoji/commands/admin'; import { askCommand } from '../../src/mahoji/commands/ask'; -import { bankCommand } from '../../src/mahoji/commands/bank'; import { bsCommand } from '../../src/mahoji/commands/bs'; import { buildCommand } from '../../src/mahoji/commands/build'; import { buyCommand } from '../../src/mahoji/commands/buy'; +import { caCommand } from '../../src/mahoji/commands/ca'; import { chooseCommand } from '../../src/mahoji/commands/choose'; import { chopCommand } from '../../src/mahoji/commands/chop'; import { claimCommand } from '../../src/mahoji/commands/claim'; import { clueCommand } from '../../src/mahoji/commands/clue'; +import { configCommand } from '../../src/mahoji/commands/config'; +import { cookCommand } from '../../src/mahoji/commands/cook'; +import { craftCommand } from '../../src/mahoji/commands/craft'; import { createCommand } from '../../src/mahoji/commands/create'; +import { dataCommand } from '../../src/mahoji/commands/data'; +import { dropCommand } from '../../src/mahoji/commands/drop'; +import { fakeCommand } from '../../src/mahoji/commands/fake'; +import { fakepmCommand } from '../../src/mahoji/commands/fakepm'; import { farmingCommand } from '../../src/mahoji/commands/farming'; import { fishCommand } from '../../src/mahoji/commands/fish'; import { fletchCommand } from '../../src/mahoji/commands/fletch'; +import { gambleCommand } from '../../src/mahoji/commands/gamble'; +import { gearCommand } from '../../src/mahoji/commands/gear'; +import { gearPresetsCommand } from '../../src/mahoji/commands/gearpresets'; +import { giftCommand } from '../../src/mahoji/commands/gift'; +import { giveawayCommand } from '../../src/mahoji/commands/giveaway'; import { gpCommand } from '../../src/mahoji/commands/gp'; +import { helpCommand } from '../../src/mahoji/commands/help'; import { huntCommand } from '../../src/mahoji/commands/hunt'; +import { inviteCommand } from '../../src/mahoji/commands/invite'; +import { minionKCommand } from '../../src/mahoji/commands/k'; import { lapsCommand } from '../../src/mahoji/commands/laps'; import { leaderboardCommand } from '../../src/mahoji/commands/leaderboard'; import { lightCommand } from '../../src/mahoji/commands/light'; import { lootCommand } from '../../src/mahoji/commands/loot'; +import { mCommand } from '../../src/mahoji/commands/m'; +import { massCommand } from '../../src/mahoji/commands/mass'; +import { mineCommand } from '../../src/mahoji/commands/mine'; import { minigamesCommand } from '../../src/mahoji/commands/minigames'; import { minionCommand } from '../../src/mahoji/commands/minion'; +import { mixCommand } from '../../src/mahoji/commands/mix'; +import { offerCommand } from '../../src/mahoji/commands/offer'; import { openCommand } from '../../src/mahoji/commands/open'; import { patreonCommand } from '../../src/mahoji/commands/patreon'; import { payCommand } from '../../src/mahoji/commands/pay'; import { pohCommand } from '../../src/mahoji/commands/poh'; +import { pollCommand } from '../../src/mahoji/commands/poll'; import { priceCommand } from '../../src/mahoji/commands/price'; import { raidCommand } from '../../src/mahoji/commands/raid'; +import { redeemCommand } from '../../src/mahoji/commands/redeem'; import { rollCommand } from '../../src/mahoji/commands/roll'; import { runecraftCommand } from '../../src/mahoji/commands/runecraft'; +import { sacrificeCommand } from '../../src/mahoji/commands/sacrifice'; +import { sellCommand } from '../../src/mahoji/commands/sell'; +import { simulateCommand } from '../../src/mahoji/commands/simulate'; import { slayerCommand } from '../../src/mahoji/commands/slayer'; import { smeltingCommand } from '../../src/mahoji/commands/smelt'; +import { smithCommand } from '../../src/mahoji/commands/smith'; import { stealCommand } from '../../src/mahoji/commands/steal'; +import { tksCommand } from '../../src/mahoji/commands/tokkulshop'; import { toolsCommand } from '../../src/mahoji/commands/tools'; -import { OSBMahojiCommand } from '../../src/mahoji/lib/util'; +import { tradeCommand } from '../../src/mahoji/commands/trade'; +import { triviaCommand } from '../../src/mahoji/commands/trivia'; +import { mahojiUseCommand } from '../../src/mahoji/commands/use'; import { randomMock } from './setup'; -import { createTestUser } from './util'; +import { createTestUser, mockClient, TestUser } from './util'; + +type CommandInput = Record; +async function generateCommandInputs(user: TestUser, options: readonly CommandOption[]): Promise { + let results: CommandInput[] = []; + const allPossibleOptions: Record = {}; -const commands: [OSBMahojiCommand, null | object][] = [ - [activitiesCommand, null], - [askCommand, null], - [bankCommand, null], - [bsCommand, null], - [clueCommand, null], - [claimCommand, null], - [farmingCommand, null], - [gpCommand, null], - [lapsCommand, null], - [leaderboardCommand, null], - [fletchCommand, null], - [fishCommand, null], - [createCommand, { item: 'asdf' }], - [chopCommand, null], - [chooseCommand, { list: 'a,a,a' }], - [buildCommand, null], - [buyCommand, null], - [huntCommand, null], - [lightCommand, null], - [lootCommand, null], - [minionCommand, null], - [minigamesCommand, null], - [runecraftCommand, { rune: 'blood rune' }], - [stealCommand, null], - [rollCommand, null], - [raidCommand, null], - [priceCommand, null], - [openCommand, null], - [patreonCommand, null], - [payCommand, { user: { user: { id: '2' } } }], - [pohCommand, null], - [slayerCommand, null], - [toolsCommand, null], - [stealCommand, null], - [smeltingCommand, null] -]; + for (const option of options) { + switch (option.type) { + case ApplicationCommandOptionType.SubcommandGroup: + case ApplicationCommandOptionType.Subcommand: + if (option.options) { + const subOptionsResults = await generateCommandInputs(user, option.options); + results.push(...subOptionsResults.map(input => ({ [option.name]: input }))); + } + break; + case ApplicationCommandOptionType.String: + if ('autocomplete' in option && option.autocomplete) { + const autoCompleteResults = await option.autocomplete('', { id: user.id } as any, {} as any); + allPossibleOptions[option.name] = shuffleArr(autoCompleteResults.map(c => c.value)).slice(0, 3); + } else if (option.choices) { + allPossibleOptions[option.name] = option.choices.map(c => c.value).slice(0, 3); + } else if (['guild_id', 'message_id'].includes(option.name)) { + allPossibleOptions[option.name] = ['157797566833098752']; + } else { + allPossibleOptions[option.name] = ['plain string']; + } + break; + case ApplicationCommandOptionType.Integer: + case ApplicationCommandOptionType.Number: + if (option.choices) { + allPossibleOptions[option.name] = option.choices.map(c => c.value); + } else { + let value = randInt(1, 10); + if (option.min_value && option.max_value) { + value = randInt(option.min_value, option.max_value); + } + allPossibleOptions[option.name] = [option.min_value, value]; + } + break; + case ApplicationCommandOptionType.Boolean: { + allPossibleOptions[option.name] = [true, false]; + break; + } + case ApplicationCommandOptionType.User: { + allPossibleOptions[option.name] = [ + { + user: { + id: '425134194436341760', + username: 'username', + bot: false + }, + member: undefined + } + ]; + break; + } + case ApplicationCommandOptionType.Channel: + case ApplicationCommandOptionType.Role: + case ApplicationCommandOptionType.Mentionable: + // results.push({ ...currentPath, [option.name]: `Any ${option.type}` }); + break; + } + } -// Don't let any of these commands create an activity -vi.mock('../../src/lib/util/addSubTaskToActivityTask', async () => { - const actual: any = await vi.importActual('../../src/lib/util/addSubTaskToActivityTask'); - return { - ...actual, - default: async (args: any) => { - console.log(`Sending ${args}`); + const sorted = Object.values(allPossibleOptions).sort((a, b) => b.length - a.length); + const longestOptions = sorted[0]?.length; + for (let i = 0; i < longestOptions; i++) { + let obj: Record = {}; + for (const [key, val] of Object.entries(allPossibleOptions)) { + obj[key] = val[i] ?? randArrItem(val); } - }; -}); + results.push(obj); + } + return results; +} + +const bank = new Bank(); +for (const item of Items.array()) { + bank.add(item.id, 100_000_000); +} -describe('All Commands Base Test', async () => { - randomMock(); - const user = await createTestUser(); - for (const [command, options] of commands) { - test(`Run ${command.name} command`, async () => { - await user.runCommand(command, options ?? {}); +test.skip( + 'All Commands Base Test', + async () => { + expect(vi.isMockFunction(handleMahojiConfirmation)).toBe(true); + const client = await mockClient(); + process.env.CLIENT_ID = client.data.id; + randomMock(); + const maxUser = await createTestUser(bank, { GP: 100_000_000_000 }); + await maxUser.max(); + await maxUser.update({ bitfield: [BitField.isModerator] }); + const store = new Store({ name: 'commands', dirs: [join('dist', 'mahoji')], checker: isValidCommand }); + await store.load(); + const currentClientSettings = await mahojiClientSettingsFetch({ construction_cost_bank: true }); + await prisma.activity.deleteMany({ + where: { + user_id: BigInt(maxUser.id) + } }); + + const ignoredCommands = [ + 'leagues', + 'bank', + 'bingo', + 'bossrecords', + 'stats', + 'clues', + 'kc', + 'simulate', + 'lvl', + 'testpotato', + 'xp', + 'wiki', + 'casket', + 'finish', + 'kill', + 'trivia', + 'ge', + 'rp', + 'cl', + 'bsominigames', + 'completion', + 'dg', + 'invention', + 'divination', + 'droprate', + 'ic', + 'kibble', + 'lottery', + 'megaduck', + 'nursery', + 'tames', + 'farming' + ]; + const cmds = [ + adminCommand, + askCommand, + bsCommand, + buildCommand, + buyCommand, + caCommand, + chooseCommand, + chopCommand, + cookCommand, + clueCommand, + configCommand, + claimCommand, + mCommand, + gpCommand, + payCommand, + craftCommand, + fishCommand, + farmingCommand, + dropCommand, + createCommand, + activitiesCommand, + dataCommand, + fakeCommand, + fakepmCommand, + fletchCommand, + gambleCommand, + gearCommand, + gearPresetsCommand, + giveawayCommand, + helpCommand, + huntCommand, + giftCommand, + inviteCommand, + minionKCommand, + lapsCommand, + leaderboardCommand, + lightCommand, + mineCommand, + massCommand, + minigamesCommand, + minionCommand, + simulateCommand, + sellCommand, + sacrificeCommand, + rollCommand, + runecraftCommand, + raidCommand, + pollCommand, + pohCommand, + priceCommand, + openCommand, + offerCommand, + mixCommand, + lootCommand, + smeltingCommand, + slayerCommand, + redeemCommand, + patreonCommand, + smithCommand, + stealCommand, + tradeCommand, + triviaCommand, + toolsCommand, + tksCommand, + mahojiUseCommand + ]; + for (const command of store.values) { + if (ignoredCommands.includes(command.name)) continue; + if (cmds.some(c => c.name === command.name)) continue; + // throw new Error( + // `If you added a new command (${command.name}), you need to put it in the allCommandsBase.test.ts file.` + // ); + } + + const ignoredSubCommands = [ + ['tools', 'patron', 'cl_bank'], + ['loot', 'view'], + ['minion', 'bankbg'] + ]; + + for (const command of cmds) { + if (ignoredCommands.includes(command.name)) continue; + const options = await generateCommandInputs(maxUser, command.options!); + outer: for (const option of options) { + for (const [parent, sub, subCommand] of ignoredSubCommands) { + if (command.name === parent && option[sub] && (subCommand ? option[sub][subCommand] : true)) { + continue outer; + } + } + try { + const res = await maxUser.runCommand(command, option); + minionActivityCache.clear(); + // console.log(`Running command ${command.name} + // Options: ${JSON.stringify(option)} + // Result: ${JSON.stringify(res).slice(0, 100)}`); + } catch (err) { + console.error( + `Failed to run command ${command.name} with options ${JSON.stringify(option)}: ${err}` + ); + throw err; + } + } + } + + await client.processActivities(); + }, + { + timeout: Time.Minute * 10 } -}); +); diff --git a/tests/integration/grandExchange.test.ts b/tests/integration/grandExchange.test.ts index 5f09a583c5..243f4a7449 100644 --- a/tests/integration/grandExchange.test.ts +++ b/tests/integration/grandExchange.test.ts @@ -69,12 +69,12 @@ describe('Grand Exchange', async () => { console.log(`Finished initializing ${amountOfUsers} users`); // Run a bunch of commands to buy/sell - const commandPromises = new PQueue({ concurrency: 10 }); + const commandPromises = new PQueue({ concurrency: 20 }); for (const user of shuffleArr(users)) { - const method = randArrItem(['buy', 'sell']); - let quantity = randArrItem(quantities); - let price = randArrItem(prices); commandPromises.add(async () => { + const method = randArrItem(['buy', 'sell']); + let quantity = randArrItem(quantities); + let price = randArrItem(prices); for (const item of itemPool) { await user.runCommand(geCommand, { [method]: { @@ -94,10 +94,7 @@ describe('Grand Exchange', async () => { for (let i = 0; i < 100; i++) { await GrandExchange.tick(); await waitForGEToBeEmpty(); - await Promise.all([ - GrandExchange.checkGECanFullFilAllListings(), - GrandExchange.extensiveVerification() - ]); + await GrandExchange.extensiveVerification(); } await waitForGEToBeEmpty(); console.log('Finished ticking 100 times'); @@ -154,7 +151,6 @@ Based on G.E data, we should have received ${data.totalTax} tax`; assert(GrandExchange.queue.size === 0, 'Queue should be empty'); }, { - repeats: 1, timeout: Time.Minute * 5 } ); diff --git a/tests/integration/memoryHarvesting.bso.test.ts b/tests/integration/memoryHarvesting.bso.test.ts index 751ac6d970..ebdfbe4c32 100644 --- a/tests/integration/memoryHarvesting.bso.test.ts +++ b/tests/integration/memoryHarvesting.bso.test.ts @@ -3,7 +3,9 @@ import { ItemBank } from 'oldschooljs/dist/meta/types'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { MemoryHarvestType } from '../../src/lib/bso/divination'; +import { convertStoredActivityToFlatActivity, prisma } from '../../src/lib/settings/prisma'; import { Gear } from '../../src/lib/structures/Gear'; +import { processPendingActivities } from '../../src/lib/Task'; import { MemoryHarvestOptions } from '../../src/lib/types/minions'; import itemID from '../../src/lib/util/itemID'; import { divinationCommand } from '../../src/mahoji/commands/divination'; @@ -32,8 +34,15 @@ describe('Divination', async () => { energy: 'Pale' } }); - const activity = await user.runActivity(); + await processPendingActivities(); await user.sync(); + const _activity = await prisma.activity.findFirst({ + where: { + user_id: BigInt(user.id), + type: 'MemoryHarvest' + } + }); + const activity = convertStoredActivityToFlatActivity(_activity!) as MemoryHarvestOptions; expect(user.skillsAsXP.divination).toBeGreaterThan(1); expect(user.skillsAsLevels.divination).toEqual(36); expect(activity.dp).toEqual(false); @@ -55,8 +64,15 @@ describe('Divination', async () => { type: MemoryHarvestType.ConvertToEnergy } }); - const activity = await user.runActivity(); + await processPendingActivities(); await user.sync(); + const _activity = await prisma.activity.findFirst({ + where: { + user_id: BigInt(user.id), + type: 'MemoryHarvest' + } + }); + const activity = convertStoredActivityToFlatActivity(_activity!) as MemoryHarvestOptions; expect(user.skillsAsXP.divination).toBeGreaterThan(1); expect(user.skillsAsLevels.divination).toEqual(32); expect(activity.dp).toEqual(false); diff --git a/tests/integration/migrateUser.test.ts b/tests/integration/migrateUser.test.ts index 8de51877d0..f94f0bb254 100644 --- a/tests/integration/migrateUser.test.ts +++ b/tests/integration/migrateUser.test.ts @@ -29,7 +29,6 @@ import { describe, expect, test, vi } from 'vitest'; import { BitField } from '../../src/lib/constants'; import { GearSetupType, UserFullGearSetup } from '../../src/lib/gear/types'; -import { GrandExchange } from '../../src/lib/grandExchange'; import { trackLoot } from '../../src/lib/lootTrack'; import { incrementMinigameScore, MinigameName } from '../../src/lib/settings/minigames'; import { prisma } from '../../src/lib/settings/prisma'; @@ -1415,9 +1414,6 @@ describe('migrate user test', async () => { } }; - await GrandExchange.totalReset(); - await GrandExchange.init(); - test('test preventing a double (clobber) robochimp migration (two bot-migration)', async () => { const sourceUserId = mockedId(); const destUserId = mockedId(); diff --git a/tests/integration/mocks.ts b/tests/integration/mocks.ts new file mode 100644 index 0000000000..6508511fc7 --- /dev/null +++ b/tests/integration/mocks.ts @@ -0,0 +1,41 @@ +import { Image } from '@napi-rs/canvas'; +import { beforeEach, vi } from 'vitest'; + +import { BankImageTask } from '../../src/lib/bankImage'; + +vi.mock('../../src/lib/util/handleMahojiConfirmation.ts', () => ({ + handleMahojiConfirmation: vi.fn() +})); +vi.mock('../../src/lib/util/interactionReply', () => ({ + deferInteraction: vi.fn(), + interactionReply: vi.fn() +})); + +vi.mock('../../src/lib/util/interactionReply', () => ({ + deferInteraction: vi.fn(), + interactionReply: vi.fn() +})); + +const mockBankImageTask = { + init: vi.fn(), + run: vi.fn(), + generateBankImage: vi.fn().mockReturnValue(Promise.resolve({ image: Buffer.from(''), isTransparent: false })), + getItemImage: vi.fn().mockReturnValue(Promise.resolve(new Image())), + fetchAndCacheImage: vi.fn().mockReturnValue(Promise.resolve(new Image())) +}; + +global.bankImageGenerator = mockBankImageTask as any; +BankImageTask.prototype.init = mockBankImageTask.init; +BankImageTask.prototype.run = mockBankImageTask.init; +BankImageTask.prototype.generateBankImage = mockBankImageTask.generateBankImage; +BankImageTask.prototype.getItemImage = mockBankImageTask.getItemImage; +BankImageTask.prototype.fetchAndCacheImage = mockBankImageTask.fetchAndCacheImage; + +beforeEach(async () => { + global.bankImageGenerator = mockBankImageTask as any; + BankImageTask.prototype.init = mockBankImageTask.init; + BankImageTask.prototype.run = mockBankImageTask.init; + BankImageTask.prototype.generateBankImage = mockBankImageTask.generateBankImage; + BankImageTask.prototype.getItemImage = mockBankImageTask.getItemImage; + BankImageTask.prototype.fetchAndCacheImage = mockBankImageTask.fetchAndCacheImage; +}); diff --git a/tests/integration/monsterKilling.bso.test.ts b/tests/integration/monsterKilling.bso.test.ts index 8d4276970e..9c74c732f8 100644 --- a/tests/integration/monsterKilling.bso.test.ts +++ b/tests/integration/monsterKilling.bso.test.ts @@ -5,7 +5,7 @@ import { convertStoredActivityToFlatActivity } from '../../src/lib/settings/pris import { Gear } from '../../src/lib/structures/Gear'; import { processPendingActivities } from '../../src/lib/Task'; import { MonsterActivityTaskOptions } from '../../src/lib/types/minions'; -import { killCommand } from '../../src/mahoji/commands/k'; +import { minionKCommand } from '../../src/mahoji/commands/k'; import { giveMaxStats } from '../../src/mahoji/commands/testpotato'; import { createTestUser, mockClient } from './util'; @@ -33,12 +33,19 @@ test('Killing Vlad', async () => { skills_hitpoints: 200_000_000 }); - await user.runCommand(killCommand, { + await user.runCommand(minionKCommand, { name: 'vladimir drakan' }); - const [finishedActivity] = await processPendingActivities(); - const data = convertStoredActivityToFlatActivity(finishedActivity) as MonsterActivityTaskOptions; + await processPendingActivities(); + await user.sync(); + const _activity = await global.prisma!.activity.findFirst({ + where: { + user_id: BigInt(user.id), + type: 'MonsterKilling' + } + }); + const data = convertStoredActivityToFlatActivity(_activity!) as MonsterActivityTaskOptions; const quantityKilled = data.quantity; expect(user.bank.amount('Shark')).toBeLessThan(1_000_000); diff --git a/tests/integration/monsterKilling.test.ts b/tests/integration/monsterKilling.test.ts index 41e2d8d929..fafe856aaf 100644 --- a/tests/integration/monsterKilling.test.ts +++ b/tests/integration/monsterKilling.test.ts @@ -1,21 +1,20 @@ import { Bank } from 'oldschooljs'; import { expect, test } from 'vitest'; -import { MonsterActivityTaskOptions } from '../../src/lib/types/minions'; -import { killCommand } from '../../src/mahoji/commands/k'; +import { minionKCommand } from '../../src/mahoji/commands/k'; import { createTestUser, mockClient } from './util'; test('Killing Men', async () => { - await mockClient(); + const client = await mockClient(); const user = await createTestUser(); const startingBank = new Bank().add('Shark', 1_000_000); await user.addItemsToBank({ items: startingBank }); await user.max(); - await user.runCommand(killCommand, { + await user.runCommand(minionKCommand, { name: 'general graardor' }); - (await user.runActivity()) as MonsterActivityTaskOptions; + await client.processActivities(); await user.sync(); expect(user.bank.amount('Shark')).toBeLessThan(1_000_000); diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index b75f8b50a9..6484b5e577 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -1,16 +1,14 @@ import '../globalSetup'; +import './mocks'; +import { Image } from '@napi-rs/canvas'; +import { noOp } from 'e'; +import mitm from 'mitm'; import { afterEach, beforeEach, vi } from 'vitest'; +import { BankImageTask, bankImageTask } from '../../src/lib/bankImage'; import { prisma } from '../../src/lib/settings/prisma'; -vi.mock('../../src/lib/util/handleMahojiConfirmation', () => ({ - handleMahojiConfirmation: vi.fn() -})); -vi.mock('../../src/lib/util/interactionReply', () => ({ - deferInteraction: vi.fn() -})); - export function randomMock(random = 0.1) { Math.random = () => random; } @@ -19,7 +17,15 @@ vi.mock('../../src/lib/util/webhook', async () => { const actual: any = await vi.importActual('../../src/lib/util/webhook'); return { ...actual, - sendToChannelID: async (_args: any) => {} + sendToChannelID: vi.fn() + }; +}); + +vi.mock('../../src/lib/gear/functions/generateGearImage', async () => { + const actual: any = await vi.importActual('../../src/lib/gear/functions/generateGearImage'); + return { + ...actual, + generateGearImage: vi.fn().mockReturnValue(Promise.resolve(Buffer.from(''))) }; }); @@ -40,8 +46,30 @@ globalClient.fetchUser = async (id: string | bigint) => ({ send: async () => {} }); +const mockBankImageTask = { + init: vi.fn(), + run: vi.fn(), + generateBankImage: vi.fn().mockReturnValue(Promise.resolve({ image: Buffer.from(''), isTransparent: false })), + getItemImage: vi.fn().mockReturnValue(Promise.resolve(new Image())), + fetchAndCacheImage: vi.fn().mockReturnValue(Promise.resolve(new Image())), + backgroundImages: [] +}; +bankImageTask.fetchAndCacheImage = mockBankImageTask.fetchAndCacheImage; +global.bankImageGenerator = mockBankImageTask as any; +BankImageTask.prototype.init = mockBankImageTask.init; +BankImageTask.prototype.run = mockBankImageTask.init; +BankImageTask.prototype.generateBankImage = mockBankImageTask.generateBankImage; +BankImageTask.prototype.getItemImage = mockBankImageTask.getItemImage; +BankImageTask.prototype.fetchAndCacheImage = mockBankImageTask.fetchAndCacheImage; + beforeEach(async () => { await prisma.$connect(); + global.bankImageGenerator = mockBankImageTask as any; + BankImageTask.prototype.init = mockBankImageTask.init; + BankImageTask.prototype.run = mockBankImageTask.init; + BankImageTask.prototype.generateBankImage = mockBankImageTask.generateBankImage; + BankImageTask.prototype.getItemImage = mockBankImageTask.getItemImage; + BankImageTask.prototype.fetchAndCacheImage = mockBankImageTask.fetchAndCacheImage; }); afterEach(async () => { @@ -49,7 +77,20 @@ afterEach(async () => { }); async function init() { - await prisma.$queryRaw`CREATE EXTENSION IF NOT EXISTS intarray;`; + await prisma.$queryRaw`CREATE EXTENSION IF NOT EXISTS intarray;`.catch(noOp); } init(); + +function setupRequestLogging() { + const mitmInstance = mitm(); + + mitmInstance.on('connect', (socket, opts) => { + if (opts?.host) { + // throw new Error(`Sending request to ${opts.host}`); + socket.bypass(); + } + }); +} + +setupRequestLogging(); diff --git a/tests/integration/trading.test.ts b/tests/integration/trading.test.ts index ac19db7f0e..31b9d56d2b 100644 --- a/tests/integration/trading.test.ts +++ b/tests/integration/trading.test.ts @@ -27,9 +27,9 @@ test('Trade consistency', async () => { checkMatch(); - for (let i = 0; i < 3; i++) { - const promises = []; + const promises = []; + for (let i = 0; i < 3; i++) { for (const user of shuffleArr(users)) { const other = randArrItem(users); const method = randArrItem(['send', 'receive', 'both']); diff --git a/tests/integration/util.ts b/tests/integration/util.ts index f06d9a5107..bfd3c337ea 100644 --- a/tests/integration/util.ts +++ b/tests/integration/util.ts @@ -5,10 +5,9 @@ import { Bank } from 'oldschooljs'; import { globalConfig } from '../../src/lib/constants'; import { MUserClass } from '../../src/lib/MUser'; -import { convertStoredActivityToFlatActivity, prisma } from '../../src/lib/settings/prisma'; +import { prisma } from '../../src/lib/settings/prisma'; import { processPendingActivities } from '../../src/lib/Task'; import { ItemBank } from '../../src/lib/types'; -import { ActivityTaskOptions } from '../../src/lib/types/minions'; import { cryptoRand } from '../../src/lib/util'; import { giveMaxStats } from '../../src/mahoji/commands/testpotato'; import { ironmanCommand } from '../../src/mahoji/lib/abstracted_commands/ironmanCommand'; @@ -22,6 +21,7 @@ export const commandRunOptions = (userID: string): Omit Promise.resolve(), editReply: () => Promise.resolve(), followUp: () => Promise.resolve() @@ -103,18 +103,6 @@ export class TestUser extends MUserClass { return this; } - async runActivity(): Promise { - const [finishedActivity] = await processPendingActivities(); - if (!finishedActivity) { - throw new Error('runActivity: No activity was ran'); - } - if (finishedActivity.user_id.toString() !== this.id) { - throw new Error('runActivity: Ran activity, but it didnt belong to this user'); - } - const data = convertStoredActivityToFlatActivity(finishedActivity); - return data as any; - } - randomBankSubset() { const bank = new Bank(); const items = shuffleArr(this.bankWithGP.items()).slice(0, randInt(0, this.bankWithGP.length)); @@ -128,7 +116,7 @@ export class TestUser extends MUserClass { const idsUsed = new Set(); export function mockedId() { - return cryptoRand(1_000_000_000, 5_000_000_000_000).toString(); + return cryptoRand(1_000_000_000_000, 5_000_000_000_000).toString(); } export async function createTestUser(bank?: Bank, userData: Partial = {}) { @@ -187,6 +175,10 @@ class TestClient { throw new Error(`Expected ${key} to be ${value} but got ${this.data[key]}`); } } + + async processActivities() { + await processPendingActivities(); + } } export async function mockClient() { diff --git a/tests/unit/images.test.ts b/tests/unit/images.test.ts index 2ed5aff1fb..4766dccfae 100644 --- a/tests/unit/images.test.ts +++ b/tests/unit/images.test.ts @@ -23,7 +23,7 @@ const toMatchImageSnapshotPlugin = configureToMatchImageSnapshot({ }); expect.extend({ toMatchImageSnapshot: toMatchImageSnapshotPlugin }); -describe('Images', () => { +describe.skip('Images', () => { test('Chat Heads', async () => { const result = await mahojiChatHead({ content: diff --git a/tests/unit/snapshots/banksnapshots.test.ts.snap b/tests/unit/snapshots/banksnapshots.test.ts.snap index 4fac0ec43a..e98f859b7a 100644 --- a/tests/unit/snapshots/banksnapshots.test.ts.snap +++ b/tests/unit/snapshots/banksnapshots.test.ts.snap @@ -965,6 +965,20 @@ exports[`BSO Buyables 1`] = ` "outputItems": undefined, "qpRequired": 172, }, + { + "gpCost": 5000, + "ironmanPrice": 500, + "itemCost": Bank { + "bank": {}, + "frozen": false, + }, + "name": "Lockpick", + "outputItems": undefined, + "skillsNeeded": { + "agility": 50, + "thieving": 50, + }, + }, { "itemCost": Bank { "bank": { @@ -9922,6 +9936,24 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, }, + { + "GPCost": 50000, + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "12783": 1, + "2572": 1, + }, + "frozen": false, + }, + "name": "Ring of wealth (i)", + "outputItems": Bank { + "bank": { + "12785": 1, + }, + "frozen": false, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -24041,7 +24073,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24062,7 +24094,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24083,7 +24115,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24104,7 +24136,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24125,7 +24157,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24146,7 +24178,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24167,7 +24199,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24188,7 +24220,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24209,7 +24241,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24230,7 +24262,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24251,7 +24283,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24272,7 +24304,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24293,7 +24325,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24314,7 +24346,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24335,7 +24367,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { @@ -24356,7 +24388,7 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, "requiredSlayerUnlocks": [ - 54, + 56, ], }, { diff --git a/tests/unit/snapshots/clsnapshots.test.ts.snap b/tests/unit/snapshots/clsnapshots.test.ts.snap index de5af7c1dc..86a1cf0e46 100644 --- a/tests/unit/snapshots/clsnapshots.test.ts.snap +++ b/tests/unit/snapshots/clsnapshots.test.ts.snap @@ -40,7 +40,7 @@ Cooking (28) Corporeal Beast (8) Crafting (175) Crazy archaeologist (3) -Creatables (750) +Creatables (751) Creature Creation (7) Custom Pets (47) Custom Pets (Discontinued) (20) @@ -145,7 +145,7 @@ Shooting Stars (2) Skilling Misc (41) Skilling Pets (8) Skotizo (6) -Slayer (80) +Slayer (83) Slayer Masks/Helms (32) Smithing (200) Solis (3) @@ -758,6 +758,9 @@ Cyclops head Daemonheim agility pass Dagannoth mask Dagannoth slayer helm +Dagon'hai hat +Dagon'hai robe bottom +Dagon'hai robe top Dark acorn Dark beast mask Dark beast slayer helm diff --git a/tests/unit/utils.ts b/tests/unit/utils.ts index 223b4e51b4..345c1083f1 100644 --- a/tests/unit/utils.ts +++ b/tests/unit/utils.ts @@ -35,7 +35,7 @@ interface MockUserArgs { export const mockUser = (overrides?: MockUserArgs): User => { const gearMelee = filterGearSetup(overrides?.meleeGear); const cl = new Bank().add(overrides?.cl ?? {}); - return { + const r = { cl, gear_fashion: new Gear().raw() as Prisma.JsonValue, gear_mage: new Gear().raw() as Prisma.JsonValue, @@ -72,7 +72,7 @@ export const mockUser = (overrides?: MockUserArgs): User => { skills_dungeoneering: 0, skills_invention: 0, skills_hitpoints: overrides?.skills_hitpoints ?? convertLVLtoXP(10), - GP: overrides?.GP, + GP: overrides?.GP ?? 0, premium_balance_tier: overrides?.premium_balance_tier, premium_balance_expiry_date: overrides?.premium_balance_expiry_date, ironman_alts: [], @@ -85,6 +85,8 @@ export const mockUser = (overrides?: MockUserArgs): User => { monsterScores: {}, skills_divination: 0 } as unknown as User; + + return r; }; class TestMUser extends MUserClass { @@ -122,6 +124,9 @@ export async function testRunCmd({ Math.random = () => 0.5; const hash = murmurhash(JSON.stringify({ name: cmd.name, opts, user })).toString(); const mockedUser = mockMUser({ id: hash, ...user }); + if (mockedUser.GP === null || Number.isNaN(mockedUser.GP) || mockedUser.GP < 0 || mockedUser.GP === undefined) { + throw new Error(`Invalid GP for user ${hash}`); + } mockUserMap.set(hash, mockedUser); const options: any = { user: mockedUser.user, diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts index 6b77ff0140..1a349326db 100644 --- a/vitest.integration.config.mts +++ b/vitest.integration.config.mts @@ -12,8 +12,8 @@ export default defineConfig({ }, testTimeout: 30_000, bail: 1, - maxConcurrency: 1, - maxWorkers: 1, + maxConcurrency: 5, + maxWorkers: 5, minWorkers: 1, pool: 'forks' } diff --git a/vitest.unit.config.mts b/vitest.unit.config.mts index 4d8e0dce89..aeb6a9abaf 100644 --- a/vitest.unit.config.mts +++ b/vitest.unit.config.mts @@ -13,6 +13,9 @@ export default defineConfig({ }, setupFiles: 'tests/unit/setup.ts', resolveSnapshotPath: (testPath, extension) => - join(join(dirname(testPath), 'snapshots'), `${basename(testPath)}${extension}`) + join(join(dirname(testPath), 'snapshots'), `${basename(testPath)}${extension}`), + maxConcurrency: 10, + maxWorkers: 5, + minWorkers: 1 } }); diff --git a/yarn.lock b/yarn.lock index cea7ae1625..b2b2d7239e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -990,6 +990,13 @@ dependencies: "@types/node" "*" +"@types/mitm@^1.3.8": + version "1.3.8" + resolved "https://registry.yarnpkg.com/@types/mitm/-/mitm-1.3.8.tgz#0acc7b82c1afd223828d445b1b101f86d6680ae8" + integrity sha512-vIXvHF4F9vJZOi9r/nOyaWFW0Y2MLcoaokN/GrZ3LYE0c2g7Y2kKpJ36hKT3EeJwzDsYZKjYyUEiCqDzSG/h9g== + dependencies: + "@types/node" "*" + "@types/node-cron@^3.0.7": version "3.0.7" resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.7.tgz#978bf75f7247385c61d23b6a060ba9eedb03e2f4" @@ -3865,6 +3872,14 @@ minipass@^7.0.4: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.0.tgz#b545f84af94e567386770159302ca113469c80b8" integrity sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig== +mitm@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/mitm/-/mitm-1.7.2.tgz#d079c44c763a333b15a0f7bfd02446fb8dbbe8e8" + integrity sha512-SuiJbc5xisP/iUYvsKAvrvPeoyJQbYI3WOfnp8A7XHDn4wkdtmGZe2ZTFXIo3K1of05oxUiaJIK+GoAU5KgFOw== + dependencies: + semver ">= 5 < 6" + underscore ">= 1.1.6 < 1.14" + mlly@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.2.1.tgz#cd50151f5712b651c5c379085157bcdff661133b" @@ -4785,7 +4800,7 @@ seedrandom@^3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -"semver@2 || 3 || 4 || 5": +"semver@2 || 3 || 4 || 5", "semver@>= 5 < 6": version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -4998,7 +5013,16 @@ stream-to-array@^2.3.0: dependencies: any-promise "^1.1.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5050,7 +5074,14 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5356,6 +5387,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +"underscore@>= 1.1.6 < 1.14": + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + undici@5.27.2: version "5.27.2" resolved "https://registry.yarnpkg.com/undici/-/undici-5.27.2.tgz#a270c563aea5b46cc0df2550523638c95c5d4411" @@ -5549,7 +5585,16 @@ word-wrap@^1.2.3, word-wrap@^1.2.5, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==