diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fca792205b..d0f71ed6fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -187,8 +187,6 @@ model ClientStorage { xmas_ironman_food_bank Json @default("{}") @db.Json - divination_is_released Boolean @default(false) - @@map("clientStorage") } @@ -757,6 +755,14 @@ model Tame { total_cost Json @default("{}") @db.Json + demonic_jibwings_saved_cost Json @default("{}") @db.JsonB + third_age_jibwings_loot Json @default("{}") @db.JsonB + abyssal_jibwings_loot Json @default("{}") @db.JsonB + implings_loot Json @default("{}") @db.JsonB + + last_activity_date DateTime? @db.Timestamp(6) + levels_from_egg_feed Int? + tame_activity TameActivity[] @@index([user_id]) diff --git a/src/lib/MUser.ts b/src/lib/MUser.ts index 668f4e419a..04b7a97735 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -1,11 +1,12 @@ import { userMention } from '@discordjs/builders'; import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; -import { Prisma, User, UserStats, xp_gains_skill_enum } from '@prisma/client'; +import { Prisma, TameActivity, User, UserStats, xp_gains_skill_enum } from '@prisma/client'; import { calcWhatPercent, objectEntries, randArrItem, sumArr, Time, uniqueArr } from 'e'; import { Bank } from 'oldschooljs'; import { EquipmentSlot, Item } from 'oldschooljs/dist/meta/types'; import { timePerAlch } from '../mahoji/lib/abstracted_commands/alchCommand'; +import { getParsedStashUnits } from '../mahoji/lib/abstracted_commands/stashUnitsCommand'; import { userStatsUpdate } from '../mahoji/mahojiSettings'; import { addXP } from './addXP'; import { GodFavourBank, GodName } from './bso/divineDominion'; @@ -956,6 +957,11 @@ GROUP BY data->>'clueID';`); return { refundBank }; } + async fetchStashUnits() { + const units = await getParsedStashUnits(this.id); + return units; + } + async validateEquippedGear() { let itemsUnequippedAndRefunded = new Bank(); for (const [gearSetupName, gearSetup] of Object.entries(this.gear) as [GearSetupType, GearSetup][]) { @@ -1000,6 +1006,32 @@ GROUP BY data->>'clueID';`); itemsUnequippedAndRefunded }; } + + async fetchTames() { + const tames = await prisma.tame.findMany({ + where: { + user_id: this.id + } + }); + return tames.map(t => new MTame(t)); + } + + async fetchActiveTame(): Promise<{ tame: null; activity: null } | { activity: TameActivity | null; tame: MTame }> { + if (!this.user.selected_tame) { + return { + tame: null, + activity: null + }; + } + const tame = await prisma.tame.findFirst({ where: { id: this.user.selected_tame } }); + if (!tame) { + throw new Error('No tame found for selected tame.'); + } + const activity = await prisma.tameActivity.findFirst({ + where: { user_id: this.id, tame_id: tame.id, completed: false } + }); + return { activity, tame: new MTame(tame) }; + } } declare global { export type MUser = MUserClass; diff --git a/src/lib/bankImage.ts b/src/lib/bankImage.ts index aad86400df..327199f96b 100644 --- a/src/lib/bankImage.ts +++ b/src/lib/bankImage.ts @@ -254,7 +254,9 @@ const forcedShortNameMap = new Map([ [i('Sanguinesti staff (uncharged)'), 'Unch.'], [i('Scythe of vitur (uncharged)'), 'Unch.'], [i('Holy scythe of vitur (uncharged)'), 'Unch.'], - [i('Sanguine scythe of vitur (uncharged)'), 'Unch.'] + [i('Sanguine scythe of vitur (uncharged)'), 'Unch.'], + + [i('Atomic energy'), 'Atomic'] ]); for (const energy of divinationEnergies) { diff --git a/src/lib/bso/divination.ts b/src/lib/bso/divination.ts index 39b1cfad8c..1842162807 100644 --- a/src/lib/bso/divination.ts +++ b/src/lib/bso/divination.ts @@ -8,6 +8,10 @@ import { hasUnlockedAtlantis } from '../util'; import getOSItem from '../util/getOSItem'; import itemID from '../util/itemID'; +export function calcEnergyPerMemory(energy: DivinationEnergy) { + return (120 - energy.level) / 150; +} + export const divinationEnergies = [ { level: 1, @@ -230,9 +234,20 @@ export const divinationEnergies = [ return null; } } -]; +] as const; + +export function calcAtomicEnergy(energy: DivinationEnergy): number { + let qty = Math.ceil(Math.pow(1 - calcEnergyPerMemory(energy), 3.5) * 250); + if (energy.type === 'Ancient') { + qty *= 2; + } + return qty; +} + +export type DivinationEnergy = (typeof divinationEnergies)[number]; for (const energy of divinationEnergies) { + // @ts-ignore Ignore energy.boonEnergyCost = energy.level * 50; } diff --git a/src/lib/compCape.ts b/src/lib/compCape.ts index 0f67dfd6ba..bb04c1da95 100644 --- a/src/lib/compCape.ts +++ b/src/lib/compCape.ts @@ -4,7 +4,6 @@ import { calcWhatPercent, objectEntries, sumArr } from 'e'; import { writeFileSync } from 'fs'; import { Bank, Items } from 'oldschooljs'; -import { tameFeedableItems } from '../mahoji/commands/tames'; import { getPOH } from '../mahoji/lib/abstracted_commands/pohCommand'; import { divinationEnergies } from './bso/divination'; import { ClueTiers } from './clues/clueTiers'; @@ -166,7 +165,7 @@ import { herbloreCL } from './skilling/skills/herblore/mixables'; import { smithingCL } from './skilling/skills/smithing/smithables'; import { slayerUnlockableRewards } from './slayer/slayerUnlocks'; import { RequirementFailure, Requirements } from './structures/Requirements'; -import { TameSpeciesID, TameType } from './tames'; +import { tameFeedableItems, TameSpeciesID } from './tames'; import { ItemBank } from './types'; import { itemID, itemNameFromID } from './util'; import resolveItems from './util/resolveItems'; @@ -695,7 +694,9 @@ const tameRequirements = new Requirements() name: 'Feed a Monkey tame all items that provide a boost', has: async ({ user }) => { const tames = await user.getTames(); - const itemsToBeFed = tameFeedableItems.filter(i => i.tameSpeciesCanBeFedThis.includes(TameType.Gatherer)); + const itemsToBeFed = tameFeedableItems.filter(i => + i.tameSpeciesCanBeFedThis.includes(TameSpeciesID.Monkey) + ); const oneTameHasAll = tames .filter(t => t.species.id === TameSpeciesID.Monkey) @@ -715,7 +716,7 @@ const tameRequirements = new Requirements() name: 'Feed a Igne tame all items that provide a boost', has: async ({ user }) => { const tames = await user.getTames(); - const itemsToBeFed = tameFeedableItems.filter(i => i.tameSpeciesCanBeFedThis.includes(TameType.Combat)); + const itemsToBeFed = tameFeedableItems.filter(i => i.tameSpeciesCanBeFedThis.includes(TameSpeciesID.Igne)); const oneTameHasAll = tames .filter(t => t.species.id === TameSpeciesID.Igne) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f1c2380857..0cb13883c3 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -322,7 +322,8 @@ export enum BitField { HasLuminousBoon = 223, HasIncandescentBoon = 224, HasVibrantBoon = 225, - HasAncientBoon = 226 + HasAncientBoon = 226, + DisabledTameClueOpening = 227 } interface BitFieldData { @@ -515,6 +516,11 @@ export const BitFieldData: Record = { name: 'Disable Item Contract donations', protected: false, userConfigurable: true + }, + [BitField.DisabledTameClueOpening]: { + name: 'Disable Eagle Tame Opening Caskets', + protected: false, + userConfigurable: true } } as const; diff --git a/src/lib/customItems/customItems.ts b/src/lib/customItems/customItems.ts index 541cdc8fa1..86731e8764 100644 --- a/src/lib/customItems/customItems.ts +++ b/src/lib/customItems/customItems.ts @@ -10901,3 +10901,614 @@ setCustomItem( ); // 73_055 Drygore axe + +setCustomItem( + 73_056, + 'Eagle egg', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_057, + 'Solite', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_058, + 'Solite platelegs', + 'Torva platelegs', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_059, + 'Solite gloves', + 'Torva gloves', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_060, + 'Solite helm', + 'Torva full helm', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_061, + 'Solite chestplate', + 'Torva platebody', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_062, + 'Solite cape', + 'Fire cape', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_063, + 'Solite boots', + 'Torva boots', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_064, + 'Solite shield', + 'Rune kiteshield', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_065, + 'Solite blade', + 'Dragon scimitar', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_066, + 'Solervus helm', + 'Rune full helm', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_067, + 'Solervus platebody', + 'Rune platebody', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_068, + 'Solervus platelegs', + 'Rune platelegs', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_069, + 'Solervus gloves', + 'Rune gloves', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_070, + 'Solervus boots', + 'Rune boots', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_071, + 'Solervus cape', + 'Fire cape', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_072, + 'Axe of the high sungod', + 'Dragon axe', + { + customItemData: { + cantDropFromMysteryBoxes: true + }, + equipment: { + slot: EquipmentSlot.TwoHanded, + attack_stab: 30, + attack_slash: 130 + 45 + 20, + attack_crush: 36 + 5, + attack_magic: -10, + attack_ranged: 0, + + defence_stab: 0, + defence_slash: 0, + defence_crush: 0, + defence_magic: 0, + defence_ranged: 0, + + melee_strength: 145, + ranged_strength: 0, + magic_damage: 0, + prayer: 3, + requirements: { + attack: 100, + strength: 100 + } + } + }, + 1 +); + +setCustomItem( + 73_073, + 'Sunlight sprouter', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_074, + 'Axe of the high sungod (u)', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_075, + 'Lunite', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_076, + 'Noom', + 'Herbi', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_077, + 'Moonlight mutator', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_078, + 'Lunite platelegs', + 'Torva platelegs', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_079, + 'Lunite gloves', + 'Torva gloves', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_080, + 'Lunite helm', + 'Torva full helm', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_081, + 'Lunite chestplate', + 'Torva platebody', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_082, + 'Lunite cape', + 'Fire cape', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_083, + 'Lunite boots', + 'Torva boots', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_084, + 'Celestial helm', + 'Rune full helm', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_085, + 'Celestial platebody', + 'Rune platebody', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_086, + 'Celestial platelegs', + 'Rune platelegs', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_087, + 'Celestial gloves', + 'Rune gloves', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_088, + 'Celestial boots', + 'Rune boots', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_089, + 'Celestial cape', + 'Fire cape', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_090, + 'Atomic energy', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_091, + 'Sun-metal scraps', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_092, + 'Sun-metal bar', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_093, + 'Axe handle base', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_094, + 'Sundial scimitar', + 'Dragon scimitar', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_095, + 'Sun-god axe head', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_096, + 'Axe handle', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_097, + 'Demonic jibwings', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_098, + 'Abyssal jibwings', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_099, + '3rd age jibwings', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_100, + 'Impling locator', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_101, + 'Divine ring', + 'Ruby ring', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_102, + 'Demonic jibwings (e)', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_103, + 'Abyssal jibwings (e)', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); + +setCustomItem( + 73_104, + '3rd age jibwings (e)', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 1 +); diff --git a/src/lib/data/Collections.ts b/src/lib/data/Collections.ts index a2a04b6b9b..c070f6f767 100644 --- a/src/lib/data/Collections.ts +++ b/src/lib/data/Collections.ts @@ -810,6 +810,12 @@ export const allCollectionLogs: ICollection = { ...Monsters.TzHaarXil.allItems ], items: tzHaarCL + }, + Solis: { + alias: ['solis'], + allItems: BSOMonsters.Solis.allItems, + items: BSOMonsters.Solis.allItems!, + fmtProg: kcProg(BSOMonsters.Solis.id) } } }, @@ -1480,7 +1486,15 @@ export const allCollectionLogs: ICollection = { 'Gorajan igne claws', 'Seamonkey staff (t1)', 'Seamonkey staff (t2)', - 'Seamonkey staff (t3)' + 'Seamonkey staff (t3)', + 'Impling locator', + 'Divine ring', + 'Abyssal jibwings (e)', + 'Demonic jibwings (e)', + '3rd age jibwings (e)', + 'Abyssal jibwings', + 'Demonic jibwings', + '3rd age jibwings' ]) }, 'Divine Dominion': { diff --git a/src/lib/data/creatables/divinationCreatables.ts b/src/lib/data/creatables/divinationCreatables.ts index dc5095c67f..afd05e4021 100644 --- a/src/lib/data/creatables/divinationCreatables.ts +++ b/src/lib/data/creatables/divinationCreatables.ts @@ -1,12 +1,22 @@ import { Bank } from 'oldschooljs'; -import { basePortentCost, divinationEnergies, portents } from '../../bso/divination'; +import { basePortentCost, calcAtomicEnergy, divinationEnergies, portents } from '../../bso/divination'; import { Createable } from '../createables'; export const divinationCreatables: Createable[] = []; for (let i = 0; i < divinationEnergies.length; i++) { const energy = divinationEnergies[i]; + + divinationCreatables.push({ + name: `Revert ${energy.item.name}`, + inputItems: new Bank().add(energy.item, 1), + outputItems: new Bank().add('Atomic energy', calcAtomicEnergy(energy)), + requiredSkills: { + divination: energy.level + } + }); + const previousEnergy = divinationEnergies[i - 1]; if (!energy.boon || !energy.boonEnergyCost) continue; if (!previousEnergy) continue; diff --git a/src/lib/data/creatables/sunMoonCreatables.ts b/src/lib/data/creatables/sunMoonCreatables.ts new file mode 100644 index 0000000000..2fcfaa7a93 --- /dev/null +++ b/src/lib/data/creatables/sunMoonCreatables.ts @@ -0,0 +1,30 @@ +import { Bank } from 'oldschooljs'; + +import { Createable } from '../createables'; + +export const sunMoonCreatables: Createable[] = [ + { + name: 'Axe handle base', + inputItems: new Bank().add('Dwarven bar').add('Volcanic shards'), + outputItems: new Bank().add('Axe handle base') + }, + { + name: 'Axe handle', + inputItems: new Bank() + .add('Axe handle base') + .add('Perfect chitin') + .add('Ent hide', 10) + .add('Athelas paste', 30), + outputItems: new Bank().add('Axe handle') + }, + { + name: 'Axe of the high sungod (u)', + inputItems: new Bank().add('Axe handle').add('Sun-god axe head'), + outputItems: new Bank().add('Axe of the high sungod (u)') + }, + { + name: 'Axe of the high sungod', + inputItems: new Bank().add('Axe of the high sungod (u)').add('Atomic energy', 2_000_000), + outputItems: new Bank().add('Axe of the high sungod') + } +]; diff --git a/src/lib/data/creatablesTable.txt b/src/lib/data/creatablesTable.txt index c694c62f52..b80105c829 100644 --- a/src/lib/data/creatablesTable.txt +++ b/src/lib/data/creatablesTable.txt @@ -1817,30 +1817,58 @@ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Revert Ghostly chicken head │ 1x Ghostly chicken head │ 90x Ghostweave │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Pale energy │ 1x Pale energy │ 2x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Flickering energy │ 1x Flickering energy │ 3x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of flickering energy │ 500x Pale energy │ 1x Boon of flickering energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Bright energy │ 1x Bright energy │ 6x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of bright energy │ 1,000x Flickering energy │ 1x Boon of bright energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Glowing energy │ 1x Glowing energy │ 11x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of glowing energy │ 1,500x Bright energy │ 1x Boon of glowing energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Sparkling energy │ 1x Sparkling energy │ 18x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of sparkling energy │ 2,000x Glowing energy │ 1x Boon of sparkling energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Gleaming energy │ 1x Gleaming energy │ 28x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of gleaming energy │ 2,500x Sparkling energy │ 1x Boon of gleaming energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Vibrant energy │ 1x Vibrant energy │ 42x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of vibrant energy │ 3,000x Gleaming energy │ 1x Boon of vibrant energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Lustrous energy │ 1x Lustrous energy │ 61x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of lustrous energy │ 3,500x Vibrant energy │ 1x Boon of lustrous energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Elder energy │ 1x Elder energy │ 72x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of elder energy │ 3,750x Lustrous energy │ 1x Boon of elder energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Brilliant energy │ 1x Brilliant energy │ 85x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of brilliant energy │ 4,000x Elder energy │ 1x Boon of brilliant energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Radiant energy │ 1x Radiant energy │ 99x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of radiant energy │ 4,250x Brilliant energy │ 1x Boon of radiant energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Luminous energy │ 1x Luminous energy │ 115x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of luminous energy │ 4,500x Radiant energy │ 1x Boon of luminous energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Incandescent energy │ 1x Incandescent energy │ 133x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of incandescent energy │ 4,750x Luminous energy │ 1x Boon of incandescent energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Revert Ancient energy │ 1x Ancient energy │ 394x Atomic energy │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Boon of ancient energy │ 5,500x Incandescent energy │ 1x Boon of ancient energy │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Cache portent │ 500x Radiant energy, 50x Molten glass, 20x Elder rune │ 1x Cache portent │ 0 ║ @@ -2465,6 +2493,22 @@ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Gorajan igne armor │ 10x Leather, 2x Gorajan shards, 1x Dwarven igne armor │ 1x Gorajan igne armor │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Demonic jibwings │ 10x Dark totem │ 1x Demonic jibwings │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Abyssal jibwings │ 10x Ancient shard, 1x Magical artifact │ 1x Abyssal jibwings │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ 3rd age jibwings │ 1,000x Gold bar │ 1x 3rd age jibwings │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ 3rd age jibwings (e) │ 1,000x Ignecarus scales, 1x 3rd age jibwings │ 1x 3rd age jibwings (e) │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Demonic jibwings (e) │ 1,000x Ignecarus scales, 1x Demonic jibwings │ 1x Demonic jibwings (e) │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Abyssal jibwings (e) │ 1,000x Ignecarus scales, 1x Abyssal jibwings │ 1x Abyssal jibwings (e) │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Divine ring │ 1,000,000x Atomic energy │ 1x Divine ring │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Impling locator │ 200,000x Atomic energy, 1,000x Elder rune │ 1x Impling locator │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Revert Runite igne claws │ 1x Runite igne claws │ 1x Igne gear frame │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Revert Dragon igne claws │ 1x Dragon igne claws │ 1x Igne gear frame │ 0 ║ @@ -2744,4 +2788,12 @@ ║ Barronite mace │ 1,500x Barronite shards, 1x Barronite head, 1x Barronite handle, 1x Barronite guard │ 1x Barronite mace │ 0 ║ ╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ ║ Imcando hammer │ 1,500x Barronite shards, 1x Imcando hammer (broken) │ 1x Imcando hammer │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Axe handle base │ 1x Dwarven bar, 1x Volcanic shards │ 1x Axe handle base │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Axe handle │ 30x Athelas paste, 10x Ent hide, 1x Perfect chitin, 1x Axe handle base │ 1x Axe handle │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Axe of the high sungod (u) │ 1x Sun-god axe head, 1x Axe handle │ 1x Axe of the high sungod (u) │ 0 ║ +╟─────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────╢ +║ Axe of the high sungod │ 2,000,000x Atomic energy, 1x Axe of the high sungod (u) │ 1x Axe of the high sungod │ 0 ║ ╚═════════════════════════════════════════════════╧════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╧════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╧═══════════╝ diff --git a/src/lib/data/createables.ts b/src/lib/data/createables.ts index 8f6527d86e..ecc5d1330d 100644 --- a/src/lib/data/createables.ts +++ b/src/lib/data/createables.ts @@ -30,6 +30,7 @@ import { nexCreatables } from './creatables/nex'; import { ornamentKits } from './creatables/ornaments'; import { shadesOfMortonCreatables } from './creatables/shadesOfMorton'; import { slayerCreatables } from './creatables/slayer'; +import { sunMoonCreatables } from './creatables/sunMoonCreatables'; import { toaCreatables } from './creatables/toa'; import { tobCreatables } from './creatables/tob'; import { tameCreatables } from './tameCreatables'; @@ -2397,7 +2398,8 @@ const Createables: Createable[] = [ ...dtCreatables, ...caCreatables, ...forestryCreatables, - ...camdozaalItems + ...camdozaalItems, + ...sunMoonCreatables ]; export default Createables; diff --git a/src/lib/data/kibble.ts b/src/lib/data/kibble.ts index b3e1991d2d..962ebaafa8 100644 --- a/src/lib/data/kibble.ts +++ b/src/lib/data/kibble.ts @@ -17,7 +17,7 @@ export const kibbles: Kibble[] = [ item: getOSItem('Simple kibble'), type: 'simple', minimumFishHeal: 1, - cropComponent: ['Cabbage', 'Potato'].map(getOSItem), + cropComponent: ['Cabbage', 'Potato', 'Avocado'].map(getOSItem), herbComponent: ['Marrentill', 'Tarromin'].map(getOSItem), xp: 600, level: 105 @@ -26,7 +26,7 @@ export const kibbles: Kibble[] = [ item: getOSItem('Delicious kibble'), type: 'delicious', minimumFishHeal: 19, - cropComponent: ['Strawberry', 'Papaya fruit'].map(getOSItem), + cropComponent: ['Strawberry', 'Papaya fruit', 'Mango'].map(getOSItem), herbComponent: ['Cadantine', 'Kwuarm'].map(getOSItem), xp: 900, level: 110 @@ -35,7 +35,7 @@ export const kibbles: Kibble[] = [ item: getOSItem('Extraordinary kibble'), type: 'extraordinary', minimumFishHeal: 26, - cropComponent: ['Orange', 'Pineapple'].map(getOSItem), + cropComponent: ['Orange', 'Pineapple', 'Lychee'].map(getOSItem), herbComponent: ['Torstol', 'Dwarf weed'].map(getOSItem), xp: 1100, level: 120 diff --git a/src/lib/data/similarItems.ts b/src/lib/data/similarItems.ts index ff9ba4de9c..a419a2c8eb 100644 --- a/src/lib/data/similarItems.ts +++ b/src/lib/data/similarItems.ts @@ -431,7 +431,10 @@ const source: [string, (string | number)[]][] = [ ['Lumberjack hat', ['Forestry hat']], ['Lumberjack top', ['Forestry top']], ['Lumberjack legs', ['Forestry legs']], - ['Lumberjack boots', ['Forestry boots']] + ['Lumberjack boots', ['Forestry boots']], + ['Abyssal jibwings', ['Abyssal jibwings (e)']], + ['3rd age jibwings', ['3rd age jibwings (e)']], + ['Demonic jibwings', ['Demonic jibwings (e)']] ]; // Make max cape count as all master capes diff --git a/src/lib/data/tameCreatables.ts b/src/lib/data/tameCreatables.ts index c8a67fc188..62e1625147 100644 --- a/src/lib/data/tameCreatables.ts +++ b/src/lib/data/tameCreatables.ts @@ -5,6 +5,95 @@ import getOSItem from '../util/getOSItem'; import resolveItems from '../util/resolveItems'; import { Createable } from './createables'; +const eagleTameCreatables: Createable[] = [ + { + name: 'Demonic jibwings', + materialCost: new MaterialBank().add('strong', 500), + inputItems: new Bank().add('Dark totem', 10), + outputItems: new Bank().add('Demonic jibwings'), + requiredSkills: { + invention: 90, + crafting: 90, + smithing: 90 + } + }, + { + name: 'Abyssal jibwings', + materialCost: new MaterialBank().add('abyssal', 30), + inputItems: new Bank().add('Ancient shard', 10).add('Magical artifact'), + outputItems: new Bank().add('Abyssal jibwings'), + requiredSkills: { + invention: 90, + crafting: 90, + smithing: 90 + } + }, + { + name: '3rd age jibwings', + materialCost: new MaterialBank().add('third-age', 6), + inputItems: new Bank().add('Gold bar', 1000), + outputItems: new Bank().add('3rd age jibwings'), + requiredSkills: { + invention: 90, + crafting: 90, + smithing: 90 + } + }, + { + name: '3rd age jibwings (e)', + inputItems: new Bank().add('3rd age jibwings').add('Ignecarus scales', 1000), + outputItems: new Bank().add('3rd age jibwings (e)'), + requiredSkills: { + invention: 105, + crafting: 105, + smithing: 105 + } + }, + { + name: 'Demonic jibwings (e)', + inputItems: new Bank().add('Demonic jibwings').add('Ignecarus scales', 1000), + outputItems: new Bank().add('Demonic jibwings (e)'), + requiredSkills: { + invention: 105, + crafting: 105, + smithing: 105 + } + }, + { + name: 'Abyssal jibwings (e)', + inputItems: new Bank().add('Abyssal jibwings').add('Ignecarus scales', 1000), + outputItems: new Bank().add('Abyssal jibwings (e)'), + requiredSkills: { + invention: 105, + crafting: 105, + smithing: 105 + } + }, + { + name: 'Divine ring', + inputItems: new Bank().add('Atomic energy', 1_000_000), + materialCost: new MaterialBank().add('precious', 10_000), + outputItems: new Bank().add('Divine ring'), + requiredSkills: { + invention: 105, + crafting: 105, + smithing: 105 + } + }, + { + name: 'Impling locator', + inputItems: new Bank().add('Atomic energy', 200_000).add('Elder rune', 1000), + materialCost: new MaterialBank().add('orikalkum', 100), + outputItems: new Bank().add('Impling locator'), + requiredSkills: { + invention: 105, + crafting: 105, + smithing: 105, + magic: 120 + } + } +]; + export const tameCreatables: Createable[] = [ { name: 'Runite igne claws', @@ -310,7 +399,8 @@ export const tameCreatables: Createable[] = [ invention: 120, crafting: 120 } - } + }, + ...eagleTameCreatables ]; for (const claw of resolveItems([ diff --git a/src/lib/implings.ts b/src/lib/implings.ts index 94b208d30d..a518e26bb1 100644 --- a/src/lib/implings.ts +++ b/src/lib/implings.ts @@ -178,7 +178,8 @@ export async function handlePassiveImplings(user: MUser, data: ActivityTaskData) if (hasScrollOfTheHunt) baseChance = Math.floor(baseChance / 2); if (user.hasEquippedOrInBank('Hunter master cape')) baseChance = Math.floor(baseChance / 2); - const impTable = implingTableByWorldLocation[activityInArea(data)](baseChance, user.usingPet('Mr. E')); + const area = activityInArea(data); + const impTable = implingTableByWorldLocation[area](baseChance, user.usingPet('Mr. E')); for (let i = 0; i < minutes; i++) { const loot = impTable.roll(); diff --git a/src/lib/leagues/eliteTasks.ts b/src/lib/leagues/eliteTasks.ts index f55b6e9aa7..de8f6e7389 100644 --- a/src/lib/leagues/eliteTasks.ts +++ b/src/lib/leagues/eliteTasks.ts @@ -2,7 +2,6 @@ import { sumArr } from 'e'; import { Bank, Monsters, Openables } from 'oldschooljs'; import { eggs } from '../../mahoji/commands/offer'; -import { tameFeedableItems } from '../../mahoji/commands/tames'; import { ZALCANO_ID } from '../constants'; import { abyssalDragonCL, @@ -40,7 +39,7 @@ import { Naxxus } from '../minions/data/killableMonsters/custom/bosses/Naxxus'; import Darts from '../skilling/skills/fletching/fletchables/darts'; import Javelins from '../skilling/skills/fletching/fletchables/javelins'; import { ashes } from '../skilling/skills/prayer'; -import { TameSpeciesID, TameType } from '../tames'; +import { tameFeedableItems, TameSpeciesID } from '../tames'; import { ItemBank } from '../types'; import { calcTotalLevel } from '../util'; import resolveItems from '../util/resolveItems'; @@ -357,7 +356,7 @@ export const eliteTasks: Task[] = [ .some(t => { const fedItems = new Bank(t.fed_items as ItemBank); return tameFeedableItems.some( - i => i.tameSpeciesCanBeFedThis.includes(TameType.Combat) && fedItems.has(i.item.id) + i => i.tameSpeciesCanBeFedThis.includes(TameSpeciesID.Igne) && fedItems.has(i.item.id) ); }); } @@ -371,7 +370,7 @@ export const eliteTasks: Task[] = [ .some(t => { const fedItems = new Bank(t.fed_items as ItemBank); return tameFeedableItems.some( - i => i.tameSpeciesCanBeFedThis.includes(TameType.Gatherer) && fedItems.has(i.item.id) + i => i.tameSpeciesCanBeFedThis.includes(TameSpeciesID.Monkey) && fedItems.has(i.item.id) ); }); } diff --git a/src/lib/leagues/masterTasks.ts b/src/lib/leagues/masterTasks.ts index 662737181d..5c1847c0aa 100644 --- a/src/lib/leagues/masterTasks.ts +++ b/src/lib/leagues/masterTasks.ts @@ -23,10 +23,10 @@ import { dungBuyables } from '../skilling/skills/dung/dungData'; import { ashes } from '../skilling/skills/prayer'; import Dwarven from '../skilling/skills/smithing/smithables/dwarven'; import { slayerUnlockableRewards } from '../slayer/slayerUnlocks'; -import { getTameSpecies } from '../tames'; import { ItemBank } from '../types'; import { calcTotalLevel } from '../util'; import resolveItems from '../util/resolveItems'; +import { getTameSpecies } from '../util/tameUtil'; import { Task } from './leaguesUtils'; export const masterTasks: Task[] = [ diff --git a/src/lib/minions/data/killableMonsters/custom/SunMoon.ts b/src/lib/minions/data/killableMonsters/custom/SunMoon.ts new file mode 100644 index 0000000000..dcd22873ef --- /dev/null +++ b/src/lib/minions/data/killableMonsters/custom/SunMoon.ts @@ -0,0 +1,136 @@ +import { Time } from 'e'; +import { Bank, LootTable, Monsters } from 'oldschooljs'; + +import { GearStat } from '../../../../gear'; +import { addStatsOfItemsTogether, Gear } from '../../../../structures/Gear'; +import resolveItems from '../../../../util/resolveItems'; +import { CustomMonster } from './customMonsters'; + +const solisMinGear = new Gear(); +solisMinGear.equip('Gorajan warrior helmet'); +solisMinGear.equip('Gorajan warrior top'); +solisMinGear.equip('Gorajan warrior legs'); +solisMinGear.equip('Gorajan warrior gloves'); +solisMinGear.equip('Gorajan warrior boots'); +solisMinGear.equip('TzKal cape'); +solisMinGear.equip("Brawler's hook necklace"); +solisMinGear.equip('Ignis ring(i)'); +solisMinGear.equip('Drygore rapier'); +solisMinGear.equip('Offhand dragon claw'); + +export const Solis: CustomMonster = { + id: 129_124, + baseMonster: Monsters.AbyssalSire, + name: 'Solis', + aliases: ['solis'], + timeToFinish: Time.Minute * 120, + hp: 3330, + table: new LootTable().every('Solite', [10, 60]).tertiary(600, 'Eagle egg').tertiary(500, 'Sun-metal scraps'), + difficultyRating: 5, + qpRequired: 2500, + healAmountNeeded: 350 * 200, + attackStyleToUse: GearStat.AttackStab, + attackStylesUsed: [GearStat.AttackStab], + levelRequirements: { + hitpoints: 120, + attack: 110, + strength: 110, + defence: 110, + magic: 110, + ranged: 110, + slayer: 110 + }, + pohBoosts: { + pool: { + 'Ancient rejuvenation pool': 5 + } + }, + deathProps: { + hardness: 0.8, + steepness: 0.999, + lowestDeathChance: 10, + highestDeathChance: 80 + }, + minimumFoodHealAmount: 22, + allItems: resolveItems(['Solite', 'Eagle egg', 'Sun-metal scraps']), + minimumGearRequirements: { + melee: { + ...solisMinGear.stats, + ranged_strength: 0, + attack_ranged: 0 + } + }, + minimumWeaponShieldStats: { + melee: addStatsOfItemsTogether(resolveItems(['Offhand dragon claw', 'Drygore rapier']), [GearStat.AttackStab]) + }, + itemCost: { + itemCost: new Bank().add('Super combat potion(4)').add('Heat res. brew', 3).add('Heat res. restore'), + qtyPerKill: 1 + }, + tameCantKill: true, + itemsRequired: resolveItems(["Combatant's cape"]), + customRequirement: async user => { + const tames = await user.fetchTames(); + const hasMaxedIgne = tames.some(tame => tame.isMaxedIgneTame()); + if (hasMaxedIgne) return null; + return 'You need to have a maxed Igne Tame (best gear, all fed items) to fight Solis.'; + }, + setupsUsed: ['melee'] +}; + +// export const Celestara: CustomMonster = { +// id: 129_126, +// baseMonster: Monsters.AbyssalSire, +// name: 'Celestara', +// aliases: ['celestara'], +// timeToFinish: Time.Minute * 100, +// hp: 3330, +// table: new LootTable().every('Lunite', [10, 60]).tertiary(300, 'Noom'), +// difficultyRating: 5, +// qpRequired: 260, +// healAmountNeeded: 250 * 200, +// attackStyleToUse: GearStat.AttackStab, +// attackStylesUsed: [GearStat.AttackStab], +// levelRequirements: { +// hitpoints: 110, +// attack: 110, +// strength: 110, +// defence: 110, +// magic: 110, +// ranged: 110, +// slayer: 110 +// }, +// pohBoosts: { +// pool: { +// 'Ancient rejuvenation pool': 5 +// } +// }, +// deathProps: { +// hardness: 0.8, +// steepness: 0.999, +// lowestDeathChance: 5, +// highestDeathChance: 70 +// }, +// minimumFoodHealAmount: 22, +// allItems: resolveItems(['Solite', 'Eagle egg', 'Sun-metal scraps']), +// minimumGearRequirements: { +// melee: { +// ...solisMinGear.stats, +// ranged_strength: 0, +// attack_ranged: 0 +// } +// }, +// minimumWeaponShieldStats: { +// melee: addStatsOfItemsTogether(resolveItems(['Offhand dragon claw', 'Drygore rapier']), [GearStat.AttackStab]) +// }, +// itemCost: { +// itemCost: new Bank().add('Super combat potion(4)').add('Heat res. brew', 3).add('Heat res. restore'), +// qtyPerKill: 1 +// }, +// tameCantKill: true, +// itemsRequired: resolveItems(["Combatant's cape"]) +// }; + +export const SunMoonMonsters = { + Solis +}; diff --git a/src/lib/minions/data/killableMonsters/custom/customMonsters.ts b/src/lib/minions/data/killableMonsters/custom/customMonsters.ts index aa42cf744e..a8c988d937 100644 --- a/src/lib/minions/data/killableMonsters/custom/customMonsters.ts +++ b/src/lib/minions/data/killableMonsters/custom/customMonsters.ts @@ -7,6 +7,7 @@ import { KillableMonster } from '../../../types'; import { customDemiBosses } from './demiBosses'; import { MiscCustomMonsters } from './misc'; import { resourceDungeonMonsters } from './resourceDungeons'; +import { SunMoonMonsters } from './SunMoon'; declare module 'oldschooljs/dist/structures/Monster' { export default interface Monster { @@ -27,7 +28,8 @@ export const customKillableMonsters: KillableMonster[] = []; export const BSOMonsters = { ...customDemiBosses, ...resourceDungeonMonsters, - ...MiscCustomMonsters + ...MiscCustomMonsters, + ...SunMoonMonsters }; for (const monster of Object.values(BSOMonsters)) { diff --git a/src/lib/minions/functions/getUserFoodFromBank.ts b/src/lib/minions/functions/getUserFoodFromBank.ts index be76f01b7e..2ceb3e51a8 100644 --- a/src/lib/minions/functions/getUserFoodFromBank.ts +++ b/src/lib/minions/functions/getUserFoodFromBank.ts @@ -2,7 +2,7 @@ import { Bank } from 'oldschooljs'; import { Eatables } from '../../data/eatables'; -function getRealHealAmount(user: MUser, healAmount: ((user: MUser) => number) | number) { +export function getRealHealAmount(user: MUser, healAmount: ((user: MUser) => number) | number) { if (typeof healAmount === 'number') { return healAmount; } @@ -54,9 +54,7 @@ export default function getUserFoodFromBank({ }); if (minimumHealAmount) { - sorted = sorted.filter(i => - typeof i.healAmount === 'number' ? i.healAmount : i.healAmount(user) >= minimumHealAmount - ); + sorted = sorted.filter(i => getRealHealAmount(user, i.healAmount) >= minimumHealAmount); } // Gets all the eatables in the user bank diff --git a/src/lib/minions/functions/removeFoodFromUser.ts b/src/lib/minions/functions/removeFoodFromUser.ts index a5e7c624e7..616a0d31b3 100644 --- a/src/lib/minions/functions/removeFoodFromUser.ts +++ b/src/lib/minions/functions/removeFoodFromUser.ts @@ -7,7 +7,7 @@ import { Emoji } from '../../constants'; import { Eatables } from '../../data/eatables'; import { GearSetupType } from '../../gear/types'; import { updateBankSetting } from '../../util/updateBankSetting'; -import getUserFoodFromBank from './getUserFoodFromBank'; +import getUserFoodFromBank, { getRealHealAmount } from './getUserFoodFromBank'; export default async function removeFoodFromUser({ user, @@ -17,7 +17,8 @@ export default async function removeFoodFromUser({ attackStylesUsed, learningPercentage, isWilderness, - unavailableBank + unavailableBank, + minimumHealAmount }: { user: MUser; totalHealingNeeded: number; @@ -27,6 +28,7 @@ export default async function removeFoodFromUser({ learningPercentage?: number; isWilderness?: boolean; unavailableBank?: Bank; + minimumHealAmount?: number; }): Promise<{ foodRemoved: Bank; reductions: string[]; reductionRatio: number }> { const originalTotalHealing = totalHealingNeeded; const rawGear = user.gear; @@ -55,15 +57,20 @@ export default async function removeFoodFromUser({ user, totalHealingNeeded, favoriteFood, - minimumHealAmount: undefined, + minimumHealAmount, isWilderness, unavailableBank }); if (!foodToRemove) { throw new UserError( - `You don't have enough food to do ${activityName}! You need enough food to heal at least ${totalHealingNeeded} HP (${healPerAction} per action). You can use these food items: ${Eatables.map( - i => i.name - ).join(', ')}.` + `You don't have enough food to do ${activityName}! You need enough food to heal at least ${totalHealingNeeded} HP (${healPerAction} per action). You can use these food items${ + minimumHealAmount ? ` (Each food item must heal atleast ${minimumHealAmount}HP)` : '' + }: ${Eatables.filter(food => { + if (!minimumHealAmount) return true; + return getRealHealAmount(user, food.healAmount) >= minimumHealAmount; + }) + .map(i => i.name) + .join(', ')}.` ); } else { await transactItems({ userID: user.id, itemsToRemove: foodToRemove }); diff --git a/src/lib/minions/types.ts b/src/lib/minions/types.ts index 7f98fb8e18..f0822fec79 100644 --- a/src/lib/minions/types.ts +++ b/src/lib/minions/types.ts @@ -136,6 +136,12 @@ export interface KillableMonster { deathProps?: Omit['0'], 'currentKC'>; diaryRequirement?: [Diary, DiaryTier]; requiredBitfield?: BitField; + + minimumFoodHealAmount?: number; + minimumWeaponShieldStats?: Partial>>; + tameCantKill?: true; + customRequirement?: (user: MUser) => Promise; + setupsUsed?: GearSetupType[]; } /* * Monsters will have an array of Consumables diff --git a/src/lib/resources/images/tames/3_sprite.png b/src/lib/resources/images/tames/3_sprite.png new file mode 100644 index 0000000000..f88942d3e6 Binary files /dev/null and b/src/lib/resources/images/tames/3_sprite.png differ diff --git a/src/lib/skilling/skills/smithing/smeltables.ts b/src/lib/skilling/skills/smithing/smeltables.ts index 2096219ad9..645e84d93b 100644 --- a/src/lib/skilling/skills/smithing/smeltables.ts +++ b/src/lib/skilling/skills/smithing/smeltables.ts @@ -85,6 +85,15 @@ const Bars: Bar[] = [ inputOres: new Bank({ 'Dwarven ore': 1, Coal: 20 }), chanceOfFail: 35, timeToUse: Time.Second * 2.4 + }, + { + name: 'Sun-metal bar', + level: 110, + xp: 150, + id: itemID('Sun-metal bar'), + inputOres: new Bank({ 'Sun-metal scraps': 1, Coal: 12 }), + chanceOfFail: 1, + timeToUse: Time.Second * 2.4 } ]; diff --git a/src/lib/skilling/skills/smithing/smithables/bsoSmithables.ts b/src/lib/skilling/skills/smithing/smithables/bsoSmithables.ts new file mode 100644 index 0000000000..366ea7d4ce --- /dev/null +++ b/src/lib/skilling/skills/smithing/smithables/bsoSmithables.ts @@ -0,0 +1,20 @@ +import { Time } from 'e'; + +import itemID from '../../../../util/itemID'; +import { SmithedItem } from '../../../types'; + +const BSOSmithables: SmithedItem[] = [ + { + name: 'Sun-god axe head', + level: 110, + xp: 5123, + id: itemID('Sun-god axe head'), + inputBars: { [itemID('Sun-metal bar')]: 2 }, + timeToUse: Time.Second * 4, + outputMultiple: 1, + qpRequired: 400, + cantBeDoubled: true + } +]; + +export default BSOSmithables; diff --git a/src/lib/skilling/skills/smithing/smithables/index.ts b/src/lib/skilling/skills/smithing/smithables/index.ts index 5b71b7a318..1f1255dbdb 100644 --- a/src/lib/skilling/skills/smithing/smithables/index.ts +++ b/src/lib/skilling/skills/smithing/smithables/index.ts @@ -1,5 +1,6 @@ import Adamant from './adamant'; import Bronze from './bronze'; +import BSOSmithables from './bsoSmithables'; import Dwarven from './dwarven'; import Gold from './gold'; import Gorajan from './gorajan'; @@ -19,7 +20,8 @@ const smithables = [ ...Adamant, ...Rune, ...Dwarven, - ...Gorajan + ...Gorajan, + ...BSOSmithables ]; export default smithables; diff --git a/src/lib/structures/Gear.ts b/src/lib/structures/Gear.ts index e34be685ff..ff0d5d98bf 100644 --- a/src/lib/structures/Gear.ts +++ b/src/lib/structures/Gear.ts @@ -24,6 +24,21 @@ export type PartialGearSetup = Partial<{ [key in EquipmentSlot]: string; }>; +export function addStatsOfItemsTogether(items: number[], statWhitelist = Object.values(GearStat)) { + const osItems = items.map(i => getOSItem(i)); + let base: Required = {} as Required; + for (const item of osItems) { + for (const stat of Object.values(GearStat)) { + let thisStat = item.equipment?.[stat] ?? 0; + if (!base[stat]) base[stat] = 0; + if (statWhitelist.includes(stat)) { + base[stat] += thisStat; + } + } + } + return base; +} + export function hasGracefulEquipped(setup: Gear) { return ( setup.hasEquipped('Agility master cape') || @@ -357,7 +372,7 @@ export const globalPresets: (GearPreset & { defaultSetup: GearSetupType })[] = [ } ]; -const baseStats: GearStats = { +export const baseStats: GearStats = { attack_stab: 0, attack_slash: 0, attack_crush: 0, diff --git a/src/lib/structures/MTame.ts b/src/lib/structures/MTame.ts index fab77267cc..c1861d3dc1 100644 --- a/src/lib/structures/MTame.ts +++ b/src/lib/structures/MTame.ts @@ -1,8 +1,11 @@ -import type { Tame } from '@prisma/client'; -import { Bank } from 'oldschooljs'; +import { type Tame, tame_growth } from '@prisma/client'; +import { round } from 'e'; +import { Bank, Items } from 'oldschooljs'; import { Item } from 'oldschooljs/dist/meta/types'; -import { Species, tameSpecies } from '../tames'; +import { getSimilarItems } from '../data/similarItems'; +import { prisma } from '../settings/prisma'; +import { Species, tameFeedableItems, tameSpecies, TameSpeciesID } from '../tames'; import type { ItemBank } from '../types'; import getOSItem from '../util/getOSItem'; @@ -15,16 +18,75 @@ export class MTame { fedItems: Bank; equippedArmor: Item | null; equippedPrimary: Item | null; + nickname: string | null; + maxSupportLevel: number; + growthLevel: number; + currentSupportLevel: number; + + private currentLevel(maxLevel: number) { + return round(maxLevel / this.growthLevel, 2); + } constructor(tame: Tame) { this.tame = tame; this.id = tame.id; + this.nickname = tame.nickname; this.userID = tame.user_id; this.species = tameSpecies.find(i => i.id === tame.species_id)!; this.growthStage = tame.growth_stage; this.fedItems = new Bank(this.tame.fed_items as ItemBank); - this.equippedArmor = tame.equipped_armor === null ? null : getOSItem(tame.equipped_armor); this.equippedPrimary = tame.equipped_primary === null ? null : getOSItem(tame.equipped_primary); + this.maxSupportLevel = tame.max_support_level; + this.growthLevel = 3 - [tame_growth.baby, tame_growth.juvenile, tame_growth.adult].indexOf(tame.growth_stage); + this.currentSupportLevel = this.currentLevel(this.maxSupportLevel); + } + + toString() { + return `${this.nickname ?? this.species.name}`; + } + + hasBeenFed(itemID: number | string) { + const { id } = Items.get(itemID)!; + const items = getSimilarItems(id); + return items.some(i => this.fedItems.has(i)); + } + + hasEquipped(item: number | string) { + const { id } = Items.get(item)!; + const items = getSimilarItems(id); + return items.some(i => this.equippedArmor?.id === i || this.equippedPrimary?.id === i); + } + + isMaxedIgneTame() { + return ( + this.species.id === TameSpeciesID.Igne && + this.growthStage === 'adult' && + this.equippedPrimary?.name === 'Gorajan igne claws' && + this.equippedArmor?.name === 'Gorajan igne armor' && + tameFeedableItems + .filter(t => t.tameSpeciesCanBeFedThis.includes(TameSpeciesID.Igne)) + .map(i => i.item.id) + .every(id => this.hasBeenFed(id)) + ); + } + + async addToStatsBank( + key: + | 'total_cost' + | 'demonic_jibwings_saved_cost' + | 'third_age_jibwings_loot' + | 'abyssal_jibwings_loot' + | 'implings_loot', + bank: Bank + ) { + await prisma.tame.update({ + where: { + id: this.id + }, + data: { + [key]: new Bank(this.tame[key] as ItemBank).add(bank).bank + } + }); } } diff --git a/src/lib/tames.ts b/src/lib/tames.ts index 5c964505e8..8363db1fc0 100644 --- a/src/lib/tames.ts +++ b/src/lib/tames.ts @@ -1,55 +1,94 @@ /* eslint-disable no-case-declarations */ -import { userMention } from '@discordjs/builders'; -import { Tame, tame_growth, TameActivity, User } from '@prisma/client'; -import { - ActionRowBuilder, - APIInteractionGuildMember, - AttachmentBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, - ChatInputCommandInteraction, - GuildMember -} from 'discord.js'; -import { increaseNumByPercent, objectEntries, randArrItem, round, Time } from 'e'; -import { Bank, Items, Misc, Monsters } from 'oldschooljs'; +import { Tame, tame_growth } from '@prisma/client'; +import { objectEntries, Time } from 'e'; +import { Bank, Misc, Monsters } from 'oldschooljs'; import { Item, ItemBank } from 'oldschooljs/dist/meta/types'; import { ChambersOfXeric, TheatreOfBlood } from 'oldschooljs/dist/simulation/misc'; -import { collectables } from '../mahoji/lib/abstracted_commands/collectCommand'; -import { mahojiUsersSettingsFetch } from '../mahoji/mahojiSettings'; -import { getSimilarItems } from './data/similarItems'; -import { trackLoot } from './lootTrack'; import killableMonsters, { NightmareMonster } from './minions/data/killableMonsters'; import { customDemiBosses } from './minions/data/killableMonsters/custom/demiBosses'; import { Planks } from './minions/data/planks'; import { KillableMonster } from './minions/types'; import { prisma } from './settings/prisma'; -import { runCommand } from './settings/settings'; -import { getTemporossLoot } from './simulation/tempoross'; -import { WintertodtCrate } from './simulation/wintertodt'; import Tanning from './skilling/skills/crafting/craftables/tanning'; -import { - assert, - calcPerHour, - calculateSimpleMonsterDeathChance, - channelIsSendable, - formatDuration, - itemNameFromID, - roll -} from './util'; +import { assert, calculateSimpleMonsterDeathChance } from './util'; import getOSItem from './util/getOSItem'; -import { getUsersTamesCollectionLog } from './util/getUsersTameCL'; import { handleSpecialCoxLoot } from './util/handleSpecialCoxLoot'; import itemID from './util/itemID'; -import { makeBankImage } from './util/makeBankImage'; import resolveItems from './util/resolveItems'; export enum TameSpeciesID { Igne = 1, - Monkey = 2 + Monkey = 2, + Eagle = 3 } +interface FeedableItem { + item: Item; + tameSpeciesCanBeFedThis: TameSpeciesID[]; + description: string; + announcementString: string; +} + +export const tameFeedableItems: FeedableItem[] = [ + { + item: getOSItem('Ori'), + description: '25% extra loot', + tameSpeciesCanBeFedThis: [TameSpeciesID.Igne], + announcementString: 'Your tame will now get 25% extra loot!' + }, + { + item: getOSItem('Zak'), + description: '+35 minutes longer max trip length', + tameSpeciesCanBeFedThis: [TameSpeciesID.Igne, TameSpeciesID.Monkey], + announcementString: 'Your tame now has a much longer max trip length!' + }, + { + item: getOSItem('Abyssal cape'), + description: '20% food reduction', + tameSpeciesCanBeFedThis: [TameSpeciesID.Igne], + announcementString: 'Your tame now has 20% food reduction!' + }, + { + item: getOSItem('Voidling'), + description: '10% faster collecting', + tameSpeciesCanBeFedThis: [TameSpeciesID.Monkey], + announcementString: 'Your tame can now collect items 10% faster thanks to the Voidling helping them teleport!' + }, + { + item: getOSItem('Ring of endurance'), + description: '10% faster collecting', + tameSpeciesCanBeFedThis: [TameSpeciesID.Monkey], + announcementString: + 'Your tame can now collect items 10% faster thanks to the Ring of endurance helping them run for longer!' + }, + { + item: getOSItem('Dwarven warhammer'), + description: '30% faster PvM', + tameSpeciesCanBeFedThis: [TameSpeciesID.Igne], + announcementString: "Your tame can now kill 30% faster! It's holding the Dwarven warhammer in its claws..." + }, + { + item: getOSItem('Mr. E'), + description: 'Chance to get 2x loot', + tameSpeciesCanBeFedThis: [TameSpeciesID.Igne, TameSpeciesID.Monkey], + announcementString: "With Mr. E's energy absorbed, your tame now has a chance at 2x loot!" + }, + { + item: getOSItem('Klik'), + description: 'Makes tanning spell faster', + tameSpeciesCanBeFedThis: [TameSpeciesID.Monkey], + announcementString: + "Your tame uses a spell to infuse Klik's fire breathing ability into itself. It can now tan hides much faster." + }, + { + item: getOSItem('Impling locator'), + description: 'Allows your tame to passively catch implings', + tameSpeciesCanBeFedThis: [TameSpeciesID.Eagle], + announcementString: 'Your tame now has the ability to find and catch implings.' + } +]; + export const seaMonkeyStaves = [ { tier: 1, @@ -113,73 +152,6 @@ export const seaMonkeySpells: SeaMonkeySpell[] = [ } ]; -interface ArbitraryTameActivity { - name: string; - id: 'Tempoross' | 'Wintertodt'; - run: (opts: { - handleFinish(res: { loot: Bank | null; message: string; user: MUser }): Promise; - user: MUser; - tame: Tame; - duration: number; - }) => Promise; - allowedTames: TameSpeciesID[]; -} - -interface ArbitraryTameActivityData { - type: ArbitraryTameActivity['id']; -} - -export const arbitraryTameActivities: ArbitraryTameActivity[] = [ - { - name: 'Tempoross', - id: 'Tempoross', - allowedTames: [TameSpeciesID.Monkey], - run: async ({ handleFinish, user, duration, tame }) => { - const quantity = Math.ceil(duration / (Time.Minute * 5)); - const loot = getTemporossLoot( - quantity, - tame.max_gatherer_level + 15, - await getUsersTamesCollectionLog(user.id) - ); - const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); - handleFinish({ - loot: itemsAdded, - message: `${user}, ${tameName( - tame - )} finished defeating Tempoross ${quantity}x times, and received ${loot}.`, - user - }); - } - }, - { - name: 'Wintertodt', - id: 'Wintertodt', - allowedTames: [TameSpeciesID.Igne], - run: async ({ handleFinish, user, duration, tame }) => { - const quantity = Math.ceil(duration / (Time.Minute * 5)); - const loot = new Bank(); - for (let i = 0; i < quantity; i++) { - loot.add( - WintertodtCrate.open({ - points: randArrItem([500, 500, 750, 1000]), - itemsOwned: user.bank.bank, - skills: user.skillsAsXP, - firemakingXP: user.skillsAsXP.firemaking - }) - ); - } - const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); - handleFinish({ - loot: itemsAdded, - message: `${user}, ${tameName( - tame - )} finished defeating Wintertodt ${quantity}x times, and received ${loot}.`, - user - }); - } - } -]; - export const igneArmors = [ { item: getOSItem('Dragon igne armor'), @@ -457,11 +429,34 @@ export interface TameTaskSpellCastingOptions { loot: ItemBank; } +export interface TameTaskClueOptions { + type: 'Clues'; + clueID: number; + quantity: number; +} + +export interface ArbitraryTameActivity { + name: string; + id: 'Tempoross' | 'Wintertodt'; + run: (opts: { + handleFinish(res: { loot: Bank | null; message: string; user: MUser }): Promise; + user: MUser; + tame: Tame; + duration: number; + }) => Promise; + allowedTames: TameSpeciesID[]; +} + +interface ArbitraryTameActivityData { + type: ArbitraryTameActivity['id']; +} + export type TameTaskOptions = | ArbitraryTameActivityData | TameTaskCombatOptions | TameTaskGathererOptions - | TameTaskSpellCastingOptions; + | TameTaskSpellCastingOptions + | TameTaskClueOptions; export const tameSpecies: Species[] = [ { @@ -514,59 +509,32 @@ export const tameSpecies: Species[] = [ .add('Elder rune', 100) .add('Astral rune', 600) .add('Coins', 10_000_000) + }, + { + id: TameSpeciesID.Eagle, + type: TameType.Support, + name: 'Eagle', + variants: [1, 2, 3], + shinyVariant: 4, + shinyChance: 60, + combatLevelRange: [5, 25], + artisanLevelRange: [1, 10], + supportLevelRange: [50, 100], + gathererLevelRange: [20, 40], + relevantLevelCategory: 'support', + hatchTime: Time.Hour * 4.5, + egg: getOSItem('Eagle egg'), + emoji: '<:EagleEgg:1201712371371085894>', + emojiID: '1201712371371085894', + mergingCost: new Bank() + .add('Solite', 150) + .add('Soul rune', 2500) + .add('Elder rune', 100) + .add('Astral rune', 600) + .add('Coins', 10_000_000) } ]; -export function tameHasBeenFed(tame: Tame, item: string | number) { - const { id } = Items.get(item)!; - const items = getSimilarItems(id); - return items.some(i => Boolean((tame.fed_items as ItemBank)[i])); -} - -export function tameGrowthLevel(tame: Tame) { - const growth = 3 - [tame_growth.baby, tame_growth.juvenile, tame_growth.adult].indexOf(tame.growth_stage); - return growth; -} - -export function getTameSpecies(tame: Tame) { - return tameSpecies.find(s => s.id === tame.species_id)!; -} - -export function getMainTameLevel(tame: Tame) { - return tameGetLevel(tame, getTameSpecies(tame).relevantLevelCategory); -} - -export function tameGetLevel(tame: Tame, type: 'combat' | 'gatherer' | 'support' | 'artisan') { - const growth = tameGrowthLevel(tame); - switch (type) { - case 'combat': - return round(tame.max_combat_level / growth, 2); - case 'gatherer': - return round(tame.max_gatherer_level / growth, 2); - case 'support': - return round(tame.max_support_level / growth, 2); - case 'artisan': - return round(tame.max_artisan_level / growth, 2); - } -} - -export function tameName(tame: Tame) { - return `${tame.nickname ?? getTameSpecies(tame).name}`; -} - -export function tameToString(tame: Tame) { - let str = `${tameName(tame)} (`; - str += [ - [tameGetLevel(tame, 'combat'), '<:combat:802136963956080650>'], - [tameGetLevel(tame, 'artisan'), '<:artisan:802136963611885569>'], - [tameGetLevel(tame, 'gatherer'), '<:gathering:802136963913613372>'] - ] - .map(([emoji, lvl]) => `${emoji}${lvl}`) - .join(' '); - str += ')'; - return str; -} - export async function addDurationToTame(tame: Tame, duration: number) { if (tame.growth_stage === tame_growth.adult) return null; const percentToAdd = duration / Time.Minute / 20; @@ -597,19 +565,8 @@ export async function addDurationToTame(tame: Tame, duration: number) { return `Your tame has grown ${percentToAdd.toFixed(2)}%!`; } -function doubleLootCheck(tame: Tame, loot: Bank) { - const hasMrE = tameHasBeenFed(tame, 'Mr. E'); - let doubleLootMsg = ''; - if (hasMrE && roll(12)) { - loot.multiply(2); - doubleLootMsg = '\n**2x Loot from Mr. E**'; - } - - return { loot, doubleLootMsg }; -} - export interface Species { - id: number; + id: TameSpeciesID; type: TameType; name: string; // Tame type within its specie @@ -639,354 +596,6 @@ export interface Species { emojiID: string; } -export function shortTameTripDesc(activity: TameActivity) { - const data = activity.data as unknown as TameTaskOptions; - switch (data.type) { - case TameType.Combat: { - const mon = tameKillableMonsters.find(i => i.id === data.monsterID); - return `Killing ${mon!.name}`; - } - case TameType.Gatherer: { - return `Collecting ${itemNameFromID(data.itemID)}`; - } - case 'SpellCasting': - return `Casting ${seaMonkeySpells.find(i => i.id === data.spellID)!.name}`; - case 'Wintertodt': { - return 'Fighting Wintertodt'; - } - case 'Tempoross': { - return 'Fighting Tempoross'; - } - } -} - -export async function runTameTask(activity: TameActivity, tame: Tame) { - async function handleFinish(res: { loot: Bank | null; message: string; user: MUser }) { - const previousTameCl = new Bank({ ...(tame.max_total_loot as ItemBank) }); - - if (res.loot) { - await prisma.tame.update({ - where: { - id: tame.id - }, - data: { - max_total_loot: previousTameCl.clone().add(res.loot.bank).bank - } - }); - } - const addRes = await addDurationToTame(tame, activity.duration); - if (addRes) res.message += `\n${addRes}`; - - const channel = globalClient.channels.cache.get(activity.channel_id); - if (!channelIsSendable(channel)) return; - channel.send({ - content: res.message, - components: [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId('REPEAT_TAME_TRIP') - .setLabel('Repeat Trip') - .setStyle(ButtonStyle.Secondary) - ) - ], - files: res.loot - ? [ - new AttachmentBuilder( - ( - await makeBankImage({ - bank: res.loot, - title: `${tameName(tame)}'s Loot`, - user: res.user, - previousCL: previousTameCl - }) - ).file.attachment - ) - ] - : undefined - }); - } - const user = await mUserFetch(activity.user_id); - - const activityData = activity.data as any as TameTaskOptions; - switch (activityData.type) { - case 'pvm': { - const { quantity, monsterID } = activityData; - const mon = tameKillableMonsters.find(i => i.id === monsterID)!; - - let killQty = quantity - activity.deaths; - if (killQty < 1) { - handleFinish({ - loot: null, - message: `${userMention(user.id)}, Your tame died in all their attempts to kill ${ - mon.name - }. Get them some better armor!`, - user - }); - return; - } - const hasOri = tameHasBeenFed(tame, 'Ori'); - - const oriIsApplying = hasOri && mon.oriWorks !== false; - // If less than 8 kills, roll 25% chance per kill - if (oriIsApplying) { - if (killQty >= 8) { - killQty = Math.ceil(increaseNumByPercent(killQty, 25)); - } else { - for (let i = 0; i < quantity; i++) { - if (roll(4)) killQty++; - } - } - } - const loot = mon.loot({ quantity: killQty, tame }); - let str = `${user}, ${tameName(tame)} finished killing ${quantity}x ${mon.name}.${ - activity.deaths > 0 ? ` ${tameName(tame)} died ${activity.deaths}x times.` : '' - }`; - const boosts = []; - if (oriIsApplying) { - boosts.push('25% extra loot (ate an Ori)'); - } - if (boosts.length > 0) { - str += `\n\n**Boosts:** ${boosts.join(', ')}.`; - } - const { doubleLootMsg } = doubleLootCheck(tame, loot); - str += doubleLootMsg; - const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); - await trackLoot({ - duration: activity.duration, - kc: activityData.quantity, - id: mon.name, - changeType: 'loot', - type: 'Monster', - totalLoot: loot, - suffix: 'tame', - users: [ - { - id: user.id, - loot: itemsAdded, - duration: activity.duration - } - ] - }); - handleFinish({ - loot: itemsAdded, - message: str, - user - }); - break; - } - case 'collect': { - const { quantity, itemID } = activityData; - const collectable = collectables.find(c => c.item.id === itemID)!; - const totalQuantity = quantity * collectable.quantity; - const loot = new Bank().add(collectable.item.id, totalQuantity); - let str = `${user}, ${tameName(tame)} finished collecting ${totalQuantity}x ${ - collectable.item.name - }. (${Math.round((totalQuantity / (activity.duration / Time.Minute)) * 60).toLocaleString()}/hr)`; - const { doubleLootMsg } = doubleLootCheck(tame, loot); - str += doubleLootMsg; - const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); - handleFinish({ - loot: itemsAdded, - message: str, - user - }); - break; - } - case 'SpellCasting': { - const spell = seaMonkeySpells.find(s => s.id === activityData.spellID)!; - const loot = new Bank(activityData.loot); - let str = `${user}, ${tameName(tame)} finished casting the ${spell.name} spell for ${formatDuration( - activity.duration - )}. ${loot - .items() - .map(([item, qty]) => `${Math.floor(calcPerHour(qty, activity.duration)).toFixed(1)}/hr ${item.name}`) - .join(', ')}`; - const { doubleLootMsg } = doubleLootCheck(tame, loot); - str += doubleLootMsg; - const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); - handleFinish({ - loot: itemsAdded, - message: str, - user - }); - break; - } - case 'Tempoross': - case 'Wintertodt': { - const act = arbitraryTameActivities.find(i => i.id === activityData.type)!; - await act.run({ handleFinish, user, tame, duration: activity.duration }); - break; - } - default: { - console.error('Unmatched tame activity type', activity.type); - break; - } - } -} - -export async function tameLastFinishedActivity(user: MUser) { - const tameID = user.user.selected_tame; - if (!tameID) return null; - return prisma.tameActivity.findFirst({ - where: { - user_id: user.id, - tame_id: tameID - }, - orderBy: { - start_date: 'desc' - } - }); -} - -export async function repeatTameTrip({ - channelID, - guildID, - user, - member, - interaction, - continueDeltaMillis -}: { - channelID: string; - guildID: string | null; - user: MUser; - member: APIInteractionGuildMember | GuildMember | null; - interaction: ButtonInteraction | ChatInputCommandInteraction; - continueDeltaMillis: number | null; -}) { - const activity = await tameLastFinishedActivity(user); - if (!activity) { - return; - } - const data = activity.data as unknown as TameTaskOptions; - switch (data.type) { - case TameType.Combat: { - const mon = tameKillableMonsters.find(i => i.id === data.monsterID); - return runCommand({ - commandName: 'tames', - args: { - kill: { - name: mon!.name - } - }, - bypassInhibitors: true, - channelID, - guildID, - user, - member, - interaction, - continueDeltaMillis - }); - } - case TameType.Gatherer: { - return runCommand({ - commandName: 'tames', - args: { - collect: { - name: getOSItem(data.itemID).name - } - }, - bypassInhibitors: true, - channelID, - guildID, - user, - member, - interaction, - continueDeltaMillis - }); - } - case 'SpellCasting': { - let args = {}; - switch (data.spellID) { - case 1: { - args = { - tan: getOSItem(data.itemID).name - }; - break; - } - case 2: { - args = { - plank_make: getOSItem(data.itemID).name - }; - break; - } - case 3: { - args = { - spin_flax: 'flax' - }; - break; - } - case 4: { - args = { - superglass_make: 'molten glass' - }; - break; - } - } - return runCommand({ - commandName: 'tames', - args: { - cast: args - }, - bypassInhibitors: true, - channelID, - guildID, - user, - member, - interaction, - continueDeltaMillis - }); - } - case 'Tempoross': - case 'Wintertodt': { - return runCommand({ - commandName: 'tames', - args: { - activity: { - name: data.type - } - }, - bypassInhibitors: true, - channelID, - guildID, - user, - member, - interaction, - continueDeltaMillis - }); - } - default: { - } - } -} - -export async function getUsersTame( - user: MUser | User -): Promise< - { tame: null; activity: null; species: null } | { tame: Tame; species: Species; activity: TameActivity | null } -> { - const selectedTame = ( - await mahojiUsersSettingsFetch(user.id, { - selected_tame: true - }) - ).selected_tame; - if (!selectedTame) { - return { - tame: null, - activity: null, - species: null - }; - } - const tame = await prisma.tame.findFirst({ where: { id: selectedTame } }); - if (!tame) { - throw new Error('No tame found for selected tame.'); - } - const activity = await prisma.tameActivity.findFirst({ - where: { user_id: user.id, tame_id: tame.id, completed: false } - }); - const species = tameSpecies.find(i => i.id === tame.species_id)!; - return { tame, activity, species }; -} - export async function createTameTask({ user, channelID, diff --git a/src/lib/tickers.ts b/src/lib/tickers.ts index ea143d09ca..2bdf08c7ae 100644 --- a/src/lib/tickers.ts +++ b/src/lib/tickers.ts @@ -4,6 +4,7 @@ import { noOp, randInt, removeFromArr, shuffleArr, Time } from 'e'; import { production } from '../config'; import { userStatsUpdate } from '../mahoji/mahojiSettings'; +import { runTameTask } from '../tasks/tames/tameTasks'; import { bossEvents, startBossEvent } from './bossEvents'; import { BitField, Channel, informationalButtons, PeakTier } from './constants'; import { GrandExchange } from './grandExchange'; @@ -14,7 +15,6 @@ import { prisma, queryCountStore } from './settings/prisma'; import { runCommand } from './settings/settings'; import { getFarmingInfo } from './skilling/functions/getFarmingInfo'; import Farming from './skilling/skills/farming'; -import { runTameTask } from './tames'; import { processPendingActivities } from './Task'; import { awaitMessageComponentInteraction, getSupportGuild, makeComponents, stringMatches } from './util'; import { farmingPatchNames, getFarmingKeyFromName } from './util/farmingHelpers'; diff --git a/src/lib/util/findBISGear.ts b/src/lib/util/findBISGear.ts new file mode 100644 index 0000000000..ff770e7a3c --- /dev/null +++ b/src/lib/util/findBISGear.ts @@ -0,0 +1,69 @@ +import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; + +import { allEquippableItems } from '../../mahoji/lib/mahojiCommandOptions'; +import { getSimilarItems } from '../data/similarItems'; +import { allDyedItems } from '../dyedItems'; +import { GearStat } from '../gear/types'; +import { Gear } from '../structures/Gear'; +import { itemNameFromID } from './smallUtils'; + +export function findBestGearSetups(stat: GearStat): Gear[] { + const finalSetups: Gear[] = []; + + const usedItems = new Set(); + + function findItem(slots: EquipmentSlot[]) { + return allEquippableItems + .filter( + i => + i.equipment?.[stat] !== undefined && + i.equipment[stat] > 0 && + !allDyedItems.includes(i.id) && + !usedItems.has(i.name) + ) + .sort((a, b) => b.equipment![stat] - a.equipment![stat]) + .find(i => { + if (!slots.includes(i.equipment!.slot)) return false; + if (usedItems.has(i.name)) return false; + for (const item of getSimilarItems(i.id)) { + usedItems.add(itemNameFromID(item)!); + } + return true; + })!; + } + + for (let i = 0; i < 5; i++) { + const gear = new Gear(); + for (const slot of [ + EquipmentSlot.Ammo, + EquipmentSlot.Cape, + EquipmentSlot.Head, + EquipmentSlot.Feet, + EquipmentSlot.Hands, + EquipmentSlot.Legs, + EquipmentSlot.Neck, + EquipmentSlot.Ring + ]) { + const item = findItem([slot]); + if (item) { + gear.equip(item); + } + } + + const firstBestWeapon = findItem([EquipmentSlot.Weapon, EquipmentSlot.TwoHanded]); + if (firstBestWeapon) { + gear.equip(firstBestWeapon); + + if (firstBestWeapon.equipment!.slot === EquipmentSlot.Weapon) { + const bestShield = findItem([EquipmentSlot.Shield]); + if (bestShield) { + gear.equip(bestShield); + } + } + } + + finalSetups.push(gear); + } + + return finalSetups; +} diff --git a/src/lib/util/globalInteractions.ts b/src/lib/util/globalInteractions.ts index 89c0a2ce3c..46d5b313bd 100644 --- a/src/lib/util/globalInteractions.ts +++ b/src/lib/util/globalInteractions.ts @@ -9,13 +9,13 @@ import { autoContract } from '../../mahoji/lib/abstracted_commands/farmingContra import { shootingStarsCommand, starCache } from '../../mahoji/lib/abstracted_commands/shootingStarsCommand'; import { Cooldowns } from '../../mahoji/lib/Cooldowns'; import { userStatsBankUpdate } from '../../mahoji/mahojiSettings'; +import { repeatTameTrip } from '../../tasks/tames/tameTasks'; import { modifyBusyCounter } from '../busyCounterCache'; import { ClueTier } from '../clues/clueTiers'; import { BitField, PerkTier } from '../constants'; import { prisma } from '../settings/prisma'; import { runCommand } from '../settings/settings'; import { toaHelpCommand } from '../simulation/toa'; -import { repeatTameTrip } from '../tames'; import { ItemBank } from '../types'; import { formatDuration, stringMatches } from '../util'; import { CACHED_ACTIVE_USER_IDS } from './cachedUserIDs'; diff --git a/src/lib/util/tameUtil.ts b/src/lib/util/tameUtil.ts new file mode 100644 index 0000000000..60aa41e405 --- /dev/null +++ b/src/lib/util/tameUtil.ts @@ -0,0 +1,170 @@ +import { Tame, tame_growth, TameActivity, User } from '@prisma/client'; +import { round } from 'e'; +import { Items } from 'oldschooljs'; + +import { mahojiUsersSettingsFetch } from '../../mahoji/mahojiSettings'; +import { ClueTiers } from '../clues/clueTiers'; +import { getSimilarItems } from '../data/similarItems'; +import { prisma } from '../settings/prisma'; +import { seaMonkeySpells, Species, tameKillableMonsters, tameSpecies, TameTaskOptions, TameType } from '../tames'; +import { ItemBank } from '../types'; +import { formatDuration, itemNameFromID } from './smallUtils'; + +export async function tameLastFinishedActivity(user: MUser) { + const tameID = user.user.selected_tame; + if (!tameID) return null; + return prisma.tameActivity.findFirst({ + where: { + user_id: user.id, + tame_id: tameID + }, + orderBy: { + start_date: 'desc' + } + }); +} + +export function shortTameTripDesc(activity: TameActivity) { + const data = activity.data as unknown as TameTaskOptions; + switch (data.type) { + case TameType.Combat: { + const mon = tameKillableMonsters.find(i => i.id === data.monsterID); + return `Killing ${mon!.name}`; + } + case TameType.Gatherer: { + return `Collecting ${itemNameFromID(data.itemID)}`; + } + case 'SpellCasting': + return `Casting ${seaMonkeySpells.find(i => i.id === data.spellID)!.name}`; + case 'Wintertodt': { + return 'Fighting Wintertodt'; + } + case 'Tempoross': { + return 'Fighting Tempoross'; + } + case 'Clues': { + return `Completing ${ClueTiers.find(i => i.scrollID === data.clueID)!.name} clues`; + } + } +} + +export function calculateMaximumTameFeedingLevelGain(tame: Tame) { + const mainLevel = getMainTameLevel(tame); + if (mainLevel >= 100) return 0; + const difference = 100 - mainLevel; + return Math.floor(difference / 2) - 1; +} + +export function tameName(tame: Tame) { + return `${tame.nickname ?? getTameSpecies(tame).name}`; +} + +export function tameToString(tame: Tame) { + let str = `${tameName(tame)} (`; + str += [ + [tameGetLevel(tame, 'combat'), '<:combat:802136963956080650>'], + [tameGetLevel(tame, 'artisan'), '<:artisan:802136963611885569>'], + [tameGetLevel(tame, 'gatherer'), '<:gathering:802136963913613372>'] + ] + .map(([emoji, lvl]) => `${emoji}${lvl}`) + .join(' '); + str += ')'; + return str; +} + +export function tameHasBeenFed(tame: Tame, item: string | number) { + const { id } = Items.get(item)!; + const items = getSimilarItems(id); + return items.some(i => Boolean((tame.fed_items as ItemBank)[i])); +} + +export function tameGrowthLevel(tame: Tame) { + const growth = 3 - [tame_growth.baby, tame_growth.juvenile, tame_growth.adult].indexOf(tame.growth_stage); + return growth; +} + +export function getTameSpecies(tame: Tame) { + return tameSpecies.find(s => s.id === tame.species_id)!; +} + +export function getMainTameLevel(tame: Tame) { + return tameGetLevel(tame, getTameSpecies(tame).relevantLevelCategory); +} + +export function tameGetLevel(tame: Tame, type: 'combat' | 'gatherer' | 'support' | 'artisan') { + const growth = tameGrowthLevel(tame); + switch (type) { + case 'combat': + return round(tame.max_combat_level / growth, 2); + case 'gatherer': + return round(tame.max_gatherer_level / growth, 2); + case 'support': + return round(tame.max_support_level / growth, 2); + case 'artisan': + return round(tame.max_artisan_level / growth, 2); + } +} + +export async function getUsersTame( + user: MUser | User | string +): Promise< + { tame: null; activity: null; species: null } | { tame: Tame; species: Species; activity: TameActivity | null } +> { + const userID = typeof user === 'string' ? user : user.id; + const selectedTame = ( + await mahojiUsersSettingsFetch(userID, { + selected_tame: true + }) + ).selected_tame; + if (!selectedTame) { + return { + tame: null, + activity: null, + species: null + }; + } + const tame = await prisma.tame.findFirst({ where: { id: selectedTame } }); + if (!tame) { + throw new Error('No tame found for selected tame.'); + } + const activity = await prisma.tameActivity.findFirst({ + where: { user_id: userID, tame_id: tame.id, completed: false } + }); + const species = tameSpecies.find(i => i.id === tame.species_id)!; + return { tame, activity, species }; +} + +export function getTameStatus(tameActivity: TameActivity | null) { + if (tameActivity) { + const currentDate = new Date().valueOf(); + const timeRemaining = `${formatDuration(tameActivity.finish_date.valueOf() - currentDate, true)} remaining`; + const activityData = tameActivity.data as any as TameTaskOptions; + switch (activityData.type) { + case TameType.Combat: + return [ + `Killing ${activityData.quantity.toLocaleString()}x ${ + tameKillableMonsters.find(m => m.id === activityData.monsterID)?.name + }`, + timeRemaining + ]; + case TameType.Gatherer: + return [`Collecting ${itemNameFromID(activityData.itemID)?.toLowerCase()}`, timeRemaining]; + case 'SpellCasting': + return [ + `Casting ${seaMonkeySpells.find(i => i.id === activityData.spellID)!.name} ${ + activityData.quantity + }x times`, + timeRemaining + ]; + case 'Tempoross': + return ['Fighting the Tempoross', timeRemaining]; + case 'Wintertodt': + return ['Fighting the Wintertodt', timeRemaining]; + case 'Clues': { + const tier = ClueTiers.find(i => i.scrollID === activityData.clueID); + return [`Completing ${tier!.name} clues`, timeRemaining]; + } + } + } + return ['Idle']; +} diff --git a/src/mahoji/commands/bsominigames.ts b/src/mahoji/commands/bsominigames.ts index 8c3e1f4b92..32b127693e 100644 --- a/src/mahoji/commands/bsominigames.ts +++ b/src/mahoji/commands/bsominigames.ts @@ -13,7 +13,6 @@ import { allGodlyItems, divineDominionCheck, divineDominionSacrificeCommand } fr import { joinGuthixianCache } from '../../lib/bso/guthixianCache'; import { fishingLocations } from '../../lib/fishingContest'; import { MaterialType } from '../../lib/invention'; -import { mahojiClientSettingsFetch } from '../../lib/util/clientSettings'; import { bonanzaCommand } from '../lib/abstracted_commands/bonanzaCommand'; import { fishingContestStartCommand, @@ -335,10 +334,6 @@ export const minigamesCommand: OSBMahojiCommand = { } = options; if (options.guthixian_cache?.join) { - const { divination_is_released } = await mahojiClientSettingsFetch({ divination_is_released: true }); - if (!divination_is_released) { - return 'Divination is not released yet!'; - } return joinGuthixianCache(klasaUser, channelID); } if (options.guthixian_cache?.stats) { diff --git a/src/mahoji/commands/config.ts b/src/mahoji/commands/config.ts index bba5b78829..ddf98f0e05 100644 --- a/src/mahoji/commands/config.ts +++ b/src/mahoji/commands/config.ts @@ -139,6 +139,10 @@ const toggles: UserConfigToggle[] = [ name: 'Disable Item Contract Donations', bit: BitField.NoItemContractDonations }, + { + name: 'Disable Eagle Tame Opening Clues', + bit: BitField.DisabledTameClueOpening + }, { name: 'Disable Clue Buttons', bit: BitField.DisableClueButtons diff --git a/src/mahoji/commands/divination.ts b/src/mahoji/commands/divination.ts index bf47191628..4681301ce1 100644 --- a/src/mahoji/commands/divination.ts +++ b/src/mahoji/commands/divination.ts @@ -17,7 +17,6 @@ import { MemoryHarvestOptions } from '../../lib/types/minions'; import { assert } from '../../lib/util'; import addSubTaskToActivityTask from '../../lib/util/addSubTaskToActivityTask'; import { calcMaxTripLength } from '../../lib/util/calcMaxTripLength'; -import { mahojiClientSettingsFetch } from '../../lib/util/clientSettings'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { formatDuration } from '../../lib/util/smallUtils'; import { memoryHarvestResult, totalTimePerRound } from '../../tasks/minions/bso/memoryHarvestActivity'; @@ -130,10 +129,6 @@ export const divinationCommand: OSBMahojiCommand = { charge_portent?: { portent: string; quantity: number }; toggle_portent?: { portent: string }; }>) => { - const { divination_is_released } = await mahojiClientSettingsFetch({ divination_is_released: true }); - if (!divination_is_released) { - return 'Divination is not released yet!'; - } const user = await mUserFetch(userID); if (options.toggle_portent) { diff --git a/src/mahoji/commands/gear.ts b/src/mahoji/commands/gear.ts index c0089fbf40..b6e8bceb0b 100644 --- a/src/mahoji/commands/gear.ts +++ b/src/mahoji/commands/gear.ts @@ -7,6 +7,7 @@ import { GearSetupType, GearSetupTypes, GearStat } from '../../lib/gear/types'; import { equipPet } from '../../lib/minions/functions/equipPet'; import { unequipPet } from '../../lib/minions/functions/unequipPet'; import { itemNameFromID } from '../../lib/util'; +import { findBestGearSetups } from '../../lib/util/findBISGear'; import { gearEquipCommand, gearStatsCommand, @@ -168,6 +169,20 @@ export const gearCommand: OSBMahojiCommand = { choices: GearSetupTypes.map(i => ({ name: toTitleCase(i), value: i })) } ] + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'best_in_slot', + description: 'View the best in slot items for a particular stat.', + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'stat', + description: 'The stat to view the BiS items for.', + required: true, + choices: Object.values(GearStat).map(k => ({ name: k, value: k })) + } + ] } ], run: async ({ @@ -188,8 +203,24 @@ export const gearCommand: OSBMahojiCommand = { pet?: { equip?: string; unequip?: string }; view?: { setup: string; text_format?: boolean }; swap?: { setup_one: GearSetupType; setup_two: GearSetupType }; + best_in_slot?: { stat: GearStat }; }>) => { const user = await mUserFetch(userID); + + if (options.best_in_slot?.stat) { + const res = findBestGearSetups(options.best_in_slot.stat); + return `These are the best in slot items for ${options.best_in_slot.stat}. + +${res + .slice(0, 10) + .map( + (setup, idx) => + `${idx + 1}. ${setup.toString()} has ${setup.getStats()[options.best_in_slot!.stat]} ${ + options.best_in_slot!.stat + }` + ) + .join('\n')}`; + } if ((options.equip || options.unequip) && !gearValidationChecks.has(userID)) { const { itemsUnequippedAndRefunded } = await user.validateEquippedGear(); if (itemsUnequippedAndRefunded.length > 0) { diff --git a/src/mahoji/commands/nursery.ts b/src/mahoji/commands/nursery.ts index e0e3eb6e9a..39b9fba263 100644 --- a/src/mahoji/commands/nursery.ts +++ b/src/mahoji/commands/nursery.ts @@ -27,6 +27,11 @@ function makeTameNickname(species: Species) { const suffixs = ['Tail', 'Foot', 'Heart', 'Monkey', 'Paw']; return `${randArrItem(prefixs)} ${randArrItem(suffixs)}`; } + case TameSpeciesID.Eagle: { + const prefixs = ['Great', 'Noble', 'Sky', 'Soaring', 'Storm']; + const suffixs = ['Claw', 'Wing', 'Feather', 'Talon', 'Beak']; + return `${randArrItem(prefixs)} ${randArrItem(suffixs)}`; + } } } diff --git a/src/mahoji/commands/rates.ts b/src/mahoji/commands/rates.ts index 3494a84366..446fa7d0c7 100644 --- a/src/mahoji/commands/rates.ts +++ b/src/mahoji/commands/rates.ts @@ -3,9 +3,11 @@ import { Time } from 'e'; import { ApplicationCommandOptionType, CommandRunOptions } from 'mahoji'; import { Bank } from 'oldschooljs'; -import { divinationEnergies, memoryHarvestTypes } from '../../lib/bso/divination'; +import { calcAtomicEnergy, divinationEnergies, memoryHarvestTypes } from '../../lib/bso/divination'; +import { ClueTiers } from '../../lib/clues/clueTiers'; import { GLOBAL_BSO_XP_MULTIPLIER, PeakTier } from '../../lib/constants'; import { inventionBoosts } from '../../lib/invention/inventions'; +import killableMonsters from '../../lib/minions/data/killableMonsters'; import { stoneSpirits } from '../../lib/minions/data/stoneSpirits'; import Agility from '../../lib/skilling/skills/agility'; import { @@ -18,7 +20,7 @@ import Mining from '../../lib/skilling/skills/mining'; import Smithing from '../../lib/skilling/skills/smithing'; import { HunterTechniqueEnum } from '../../lib/skilling/types'; import { Gear } from '../../lib/structures/Gear'; -import { convertBankToPerHourStats } from '../../lib/util'; +import { convertBankToPerHourStats, stringMatches } from '../../lib/util'; import { calcMaxTripLength } from '../../lib/util/calcMaxTripLength'; import { deferInteraction } from '../../lib/util/interactionReply'; import itemID from '../../lib/util/itemID'; @@ -31,11 +33,25 @@ import { calculateMiningResult } from '../../tasks/minions/miningActivity'; import { OSBMahojiCommand } from '../lib/util'; import { calculateHunterInput } from './hunt'; import { calculateMiningInput } from './mine'; +import { determineTameClueResult } from './tames'; export const ratesCommand: OSBMahojiCommand = { name: 'rates', description: 'Check rates of various skills/activities.', options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + name: 'tames', + description: 'Check tames rates.', + options: [ + { + type: ApplicationCommandOptionType.Subcommand, + name: 'eagle', + description: 'Eagle tame.', + options: [] + } + ] + }, { type: ApplicationCommandOptionType.SubcommandGroup, name: 'xphr', @@ -72,6 +88,26 @@ export const ratesCommand: OSBMahojiCommand = { options: [] } ] + }, + { + type: ApplicationCommandOptionType.SubcommandGroup, + name: 'monster', + description: 'Check monster loot rates.', + options: [ + { + type: ApplicationCommandOptionType.Subcommand, + name: 'monster', + description: 'Check monster.', + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'name', + description: 'The name of the monster.', + required: true + } + ] + } + ] } ], run: async ({ @@ -80,10 +116,59 @@ export const ratesCommand: OSBMahojiCommand = { interaction }: CommandRunOptions<{ xphr?: { divination_memory_harvesting?: {}; agility?: {}; dungeoneering?: {}; mining?: {}; hunter?: {} }; + monster?: { monster?: { name: string } }; + tames?: { eagle?: {} }; }>) => { await deferInteraction(interaction); const user = await mUserFetch(userID); + if (options.tames?.eagle) { + let results = `${['Support Level', 'Clue Tier', 'Clues/hr', 'Kibble/hr', 'GMC/Hr'].join('\t')}\n`; + for (const tameLevel of [50, 60, 70, 75, 80, 85, 90, 95, 100]) { + for (const clueTier of ClueTiers) { + const res = determineTameClueResult({ + tameGrowthLevel: 3, + clueTier, + extraTripLength: Time.Hour * 10, + supportLevel: tameLevel, + equippedArmor: itemID('Abyssal jibwings (e)'), + equippedPrimary: itemID('Divine ring') + }); + + results += [ + tameLevel, + clueTier.name, + calcPerHour(res.quantity, res.duration).toLocaleString(), + calcPerHour(res.cost.amount('Extraordinary kibble'), res.duration).toLocaleString(), + calcPerHour(res.cost.amount('Clue scroll (grandmaster)'), res.duration).toLocaleString() + ].join('\t'); + results += '\n'; + } + } + + return { + content: 'Assumes abyssal jibwings (e) and divine ring', + ...(returnStringOrFile(results, true) as InteractionReplyOptions) + }; + } + + if (options.monster?.monster) { + const monster = killableMonsters.find(m => stringMatches(m.name, options.monster!.monster!.name)); + if (!monster) { + return 'HUH?'; + } + const sampleSize = 100_000; + const loot = monster.table.kill(sampleSize, {}); + let totalTime = monster.timeToFinish * sampleSize; + + let str = "''"; + for (const [item, qty] of loot.items()) { + const perHour = calcPerHour(qty, totalTime); + str += `${item.name}: ${perHour}/hr\n`; + } + + return str; + } if (options.xphr?.hunter) { let results = `${[ 'Creature', @@ -349,7 +434,8 @@ export const ratesCommand: OSBMahojiCommand = { 'EnergyLoot/hr', 'EnergyCost/hr', 'Energy per memory', - 'Hours for Boon' + 'Hours for Boon', + 'Atomic energy/hr' ].join('\t')}\n`; for (const energy of divinationEnergies) { for (const harvestMethod of memoryHarvestTypes) { @@ -374,7 +460,8 @@ export const ratesCommand: OSBMahojiCommand = { rounds }); - const energyPerHour = calcPerHour(res.loot.amount(energy.item.id), Time.Hour); + const energyReceived = res.loot.amount(energy.item.id); + const energyPerHour = calcPerHour(energyReceived, Time.Hour); const nextEnergy = divinationEnergies[divinationEnergies.indexOf(energy) + 1]; let timeToGetBoon = 0; @@ -387,6 +474,13 @@ export const ratesCommand: OSBMahojiCommand = { timeToGetBoon = nextEnergy.boonEnergyCost / energyPerHour; } + const atomicEnergyPerHour = + energyReceived === 0 + ? '0' + : calcPerHour(energyReceived * calcAtomicEnergy(energy), duration).toFixed( + 1 + ); + results += [ energy.type, harvestMethod.name, @@ -403,7 +497,8 @@ export const ratesCommand: OSBMahojiCommand = { energyPerHour, calcPerHour(res.cost.amount(energy.item.id), Time.Hour), res.energyPerMemory, - timeToGetBoon + timeToGetBoon, + atomicEnergyPerHour ].join('\t'); results += '\n'; } diff --git a/src/mahoji/commands/smelt.ts b/src/mahoji/commands/smelt.ts index a2efce1689..5df608719e 100644 --- a/src/mahoji/commands/smelt.ts +++ b/src/mahoji/commands/smelt.ts @@ -139,7 +139,7 @@ export const smeltingCommand: OSBMahojiCommand = { const maxCanDo = user.bank.fits(itemsNeeded); if (maxCanDo === 0) { - return "You don't have enough supplies to smelt even one of this item!"; + return `You don't have enough supplies to smelt even one of this item! You need: ${itemsNeeded}.`; } if (maxCanDo < quantity) { quantity = maxCanDo; diff --git a/src/mahoji/commands/tames.ts b/src/mahoji/commands/tames.ts index 3a093df50f..fc34e9c002 100644 --- a/src/mahoji/commands/tames.ts +++ b/src/mahoji/commands/tames.ts @@ -1,7 +1,7 @@ -import { time } from '@discordjs/builders'; +import { bold, time } from '@discordjs/builders'; import { Canvas, Image, loadImage, SKRSContext2D } from '@napi-rs/canvas'; import { mentionCommand } from '@oldschoolgg/toolkit'; -import { Tame, tame_growth, TameActivity } from '@prisma/client'; +import { Tame, tame_growth } from '@prisma/client'; import { toTitleCase } from '@sapphire/utilities'; import { ChatInputCommandInteraction, User } from 'discord.js'; import { @@ -21,6 +21,7 @@ import { CommandResponse } from 'mahoji/dist/lib/structures/ICommand'; import { Bank } from 'oldschooljs'; import { Item, ItemBank } from 'oldschooljs/dist/meta/types'; +import { ClueTier, ClueTiers } from '../../lib/clues/clueTiers'; import { badges, PerkTier } from '../../lib/constants'; import { Eatables } from '../../lib/data/eatables'; import { getSimilarItems } from '../../lib/data/similarItems'; @@ -32,27 +33,21 @@ import { prisma } from '../../lib/settings/prisma'; import Tanning from '../../lib/skilling/skills/crafting/craftables/tanning'; import { SkillsEnum } from '../../lib/skilling/types'; import { - arbitraryTameActivities, createTameTask, getIgneTameKC, - getMainTameLevel, - getTameSpecies, - getUsersTame, igneArmors, SeaMonkeySpell, seaMonkeySpells, seaMonkeyStaves, - tameGrowthLevel, - tameHasBeenFed, + tameFeedableItems, TameKillableMonster, tameKillableMonsters, - tameName, tameSpecies, TameSpeciesID, - TameTaskOptions, TameType } from '../../lib/tames'; import { + assert, exponentialPercentScale, formatDuration, formatSkillRequirements, @@ -68,7 +63,18 @@ import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmatio import { makeBankImage } from '../../lib/util/makeBankImage'; import { parseStringBank } from '../../lib/util/parseStringBank'; import resolveItems from '../../lib/util/resolveItems'; +import { + calculateMaximumTameFeedingLevelGain, + getMainTameLevel, + getTameSpecies, + getTameStatus, + getUsersTame, + tameGrowthLevel, + tameHasBeenFed, + tameName +} from '../../lib/util/tameUtil'; import { updateBankSetting } from '../../lib/util/updateBankSetting'; +import { arbitraryTameActivities } from '../../tasks/tames/tameTasks'; import { collectables } from '../lib/abstracted_commands/collectCommand'; import { OSBMahojiCommand } from '../lib/util'; @@ -81,6 +87,7 @@ async function tameAutocomplete(value: string, user: User) { } }); return tames + .sort(sortTames) .map(t => { const { relevantLevelCategory, name } = tameSpecies.find(i => i.id === t.species_id)!; return { @@ -133,76 +140,55 @@ const igneClaws = [ } ].map(i => ({ ...i, tameSpecies: [TameSpeciesID.Igne], slot: 'equipped_primary' as const })); -export const tameEquippables: TameEquippable[] = [ - ...igneClaws, - ...igneArmors, - ...seaMonkeyStaves.map(i => ({ - item: i.item, - tameSpecies: [TameSpeciesID.Monkey], - slot: 'equipped_primary' as const - })) -]; - -interface FeedableItem { - item: Item; - tameSpeciesCanBeFedThis: TameType[]; - description: string; - announcementString: string; -} - -export const tameFeedableItems: FeedableItem[] = [ - { - item: getOSItem('Ori'), - description: '25% extra loot', - tameSpeciesCanBeFedThis: [TameType.Combat], - announcementString: 'Your tame will now get 25% extra loot!' - }, +const eagleEquippables: TameEquippable[] = [ { - item: getOSItem('Zak'), - description: '+35 minutes longer max trip length', - tameSpeciesCanBeFedThis: [TameType.Combat, TameType.Gatherer], - announcementString: 'Your tame now has a much longer max trip length!' + item: getOSItem('Demonic jibwings'), + slot: 'equipped_armor', + tameSpecies: [TameSpeciesID.Eagle] }, { - item: getOSItem('Abyssal cape'), - description: '20% food reduction', - tameSpeciesCanBeFedThis: [TameType.Combat], - announcementString: 'Your tame now has 20% food reduction!' + item: getOSItem('Abyssal jibwings'), + slot: 'equipped_armor', + tameSpecies: [TameSpeciesID.Eagle] }, { - item: getOSItem('Voidling'), - description: '10% faster collecting', - tameSpeciesCanBeFedThis: [TameType.Gatherer], - announcementString: 'Your tame can now collect items 10% faster thanks to the Voidling helping them teleport!' + item: getOSItem('3rd age jibwings'), + slot: 'equipped_armor', + tameSpecies: [TameSpeciesID.Eagle] }, { - item: getOSItem('Ring of endurance'), - description: '10% faster collecting', - tameSpeciesCanBeFedThis: [TameType.Gatherer], - announcementString: - 'Your tame can now collect items 10% faster thanks to the Ring of endurance helping them run for longer!' + item: getOSItem('Demonic jibwings (e)'), + slot: 'equipped_armor', + tameSpecies: [TameSpeciesID.Eagle] }, { - item: getOSItem('Dwarven warhammer'), - description: '30% faster PvM', - tameSpeciesCanBeFedThis: [TameType.Combat], - announcementString: "Your tame can now kill 30% faster! It's holding the Dwarven warhammer in its claws..." + item: getOSItem('Abyssal jibwings (e)'), + slot: 'equipped_armor', + tameSpecies: [TameSpeciesID.Eagle] }, { - item: getOSItem('Mr. E'), - description: 'Chance to get 2x loot', - tameSpeciesCanBeFedThis: [TameType.Combat, TameType.Gatherer, TameType.Artisan, TameType.Support], - announcementString: "With Mr. E's energy absorbed, your tame now has a chance at 2x loot!" + item: getOSItem('3rd age jibwings (e)'), + slot: 'equipped_armor', + tameSpecies: [TameSpeciesID.Eagle] }, { - item: getOSItem('Klik'), - description: 'Makes tanning spell faster', - tameSpeciesCanBeFedThis: [TameType.Gatherer], - announcementString: - "Your tame uses a spell to infuse Klik's fire breathing ability into itself. It can now tan hides much faster." + item: getOSItem('Divine ring'), + slot: 'equipped_primary', + tameSpecies: [TameSpeciesID.Eagle] } ]; +export const tameEquippables: TameEquippable[] = [ + ...igneClaws, + ...igneArmors, + ...seaMonkeyStaves.map(i => ({ + item: i.item, + tameSpecies: [TameSpeciesID.Monkey], + slot: 'equipped_primary' as const + })), + ...eagleEquippables +]; + const feedingEasterEggs: [Bank, number, tame_growth[], string][] = [ [new Bank().add('Vial of water'), 2, [tame_growth.baby], 'https://imgur.com/pYjshTg'], [new Bank().add('Bread'), 2, [tame_growth.baby, tame_growth.juvenile], 'https://i.imgur.com/yldSKLZ.mp4'], @@ -306,16 +292,30 @@ function drawText(ctx: SKRSContext2D, text: string, x: number, y: number) { fillTextXTimesInCtx(ctx, text, x, y); } +function sortTames(tameA: Tame, tameB: Tame): number { + const species = tameSpecies.find(i => i.id === tameA.species_id)!; + if (tameA.species_variant === species.shinyVariant) return -1; + if (tameB.species_variant === species.shinyVariant) return 1; + if (tameA.last_activity_date && !tameB.last_activity_date) return -1; + if (!tameA.last_activity_date && tameB.last_activity_date) return 1; + if (tameA.last_activity_date && tameB.last_activity_date) { + return tameB.last_activity_date.valueOf() - tameA.last_activity_date.valueOf(); + } + // Fallback to sorting by max_combat_level if no last_activity_date for both + return getMainTameLevel(tameB) - getMainTameLevel(tameA); +} export async function tameImage(user: MUser): CommandResponse { const userTames = await prisma.tame.findMany({ where: { user_id: user.id }, orderBy: { - id: 'asc' + last_activity_date: 'desc' } }); + userTames.sort(sortTames); + if (userTames.length === 0) { return "You don't have any tames."; } @@ -447,7 +447,7 @@ export async function tameImage(user: MUser): CommandResponse { // Draw tame boosts let prevWidth = 0; let feedQty = 0; - for (const { item } of tameFeedableItems.filter(f => f.tameSpeciesCanBeFedThis.includes(species.type))) { + for (const { item } of tameFeedableItems.filter(f => f.tameSpeciesCanBeFedThis.includes(species.id))) { if (tameHasBeenFed(t, item.id)) { const itemImage = await bankImageGenerator.getItemImage(item.id); if (itemImage) { @@ -586,37 +586,6 @@ export async function removeRawFood({ }; } -export function getTameStatus(tameActivity: TameActivity | null) { - if (tameActivity) { - const currentDate = new Date().valueOf(); - const timeRemaining = `${formatDuration(tameActivity.finish_date.valueOf() - currentDate, true)} remaining`; - const activityData = tameActivity.data as any as TameTaskOptions; - switch (activityData.type) { - case TameType.Combat: - return [ - `Killing ${activityData.quantity.toLocaleString()}x ${ - tameKillableMonsters.find(m => m.id === activityData.monsterID)?.name - }`, - timeRemaining - ]; - case TameType.Gatherer: - return [`Collecting ${itemNameFromID(activityData.itemID)?.toLowerCase()}`, timeRemaining]; - case 'SpellCasting': - return [ - `Casting ${seaMonkeySpells.find(i => i.id === activityData.spellID)!.name} ${ - activityData.quantity - }x times`, - timeRemaining - ]; - case 'Tempoross': - return ['Fighting the Tempoross', timeRemaining]; - case 'Wintertodt': - return ['Fighting the Wintertodt', timeRemaining]; - } - } - return ['Idle']; -} - async function setNameCommand(user: MUser, name: string) { if (!name || name.length < 2 || name.length > 30 || ['\n', '`', '@', '<', ':'].some(char => name.includes(char))) { return "That's not a valid name for your tame."; @@ -773,9 +742,7 @@ async function feedCommand(interaction: ChatInputCommandInteraction, user: MUser bankToAdd.add(item.id, qtyToUse); } - const thisTameSpecialFeedableItems = tameFeedableItems.filter(f => - f.tameSpeciesCanBeFedThis.includes(species!.type) - ); + const thisTameSpecialFeedableItems = tameFeedableItems.filter(f => f.tameSpeciesCanBeFedThis.includes(species!.id)); if (!str || bankToAdd.length === 0) { const image = await makeBankImage({ @@ -795,11 +762,64 @@ async function feedCommand(interaction: ChatInputCommandInteraction, user: MUser return "You don't have enough items."; } + // Egg feeding + const tameEggs = tameSpecies.map(t => t.egg.id); + const eggBeingFed = tameEggs.find(egg => bankToAdd.has(egg)); + if (eggBeingFed && bankToAdd.has(eggBeingFed)) { + if (bankToAdd.length !== 1) { + return "Your tame can't eat anything else with the egg."; + } + if (tame.growth_stage !== tame_growth.adult) { + return 'Your tame is too young to eat the egg.'; + } + if (typeof tame.levels_from_egg_feed === 'number') { + return `Your tame has already eaten an egg, it can't eat another one. It gained ${tame.levels_from_egg_feed} levels from the egg.`; + } + + const levelsCanGain = calculateMaximumTameFeedingLevelGain(tame); + if (levelsCanGain < 1) { + return "Your tame isn't interested in eating the egg."; + } + + const levelRange = [0, levelsCanGain]; + await handleMahojiConfirmation( + interaction, + `Are you sure you want to feed the egg to your tame? You cannot get the egg back, and you cannot feed this tame an egg more than once. + +Your tame will gain between (inclusively) ${levelRange[0]} and ${levelRange[1]} levels from the egg.` + ); + const gained = randInt(levelRange[0], levelRange[1]); + await user.removeItemsFromBank(bankToAdd); + + await prisma.tame.update({ + where: { + id: tame.id + }, + data: { + levels_from_egg_feed: gained, + [`max_${species!.relevantLevelCategory}_level`]: { + increment: gained + } + } + }); + + await prisma.tame.update({ + where: { + id: tame.id + }, + data: { + fed_items: new Bank().add(tame.fed_items as ItemBank).add(bankToAdd).bank + } + }); + + return `You fed ${bankToAdd} to ${tameName(tame)}. It gained ${bold(gained.toString())} levels from the egg!`; + } + let specialStrArr = []; for (const { item, description, tameSpeciesCanBeFedThis } of thisTameSpecialFeedableItems) { const similarItems = getSimilarItems(item.id); if (similarItems.some(si => bankToAdd.has(si))) { - if (!tameSpeciesCanBeFedThis.includes(species!.type)) { + if (!tameSpeciesCanBeFedThis.includes(species!.id)) { await handleMahojiConfirmation( interaction, `Feeding a '${item.name}' to your tame won't give it a perk, are you sure you want to?` @@ -864,6 +884,9 @@ async function killCommand(user: MUser, channelID: string, str: string) { const monster = tameKillableMonsters.find( i => stringMatches(i.name, str) || i.aliases.some(alias => stringMatches(alias, str)) ); + if (monster?.tameCantKill) { + return 'Tames cannot kill this monster.'; + } if (!monster) return "That's not a valid monster."; if (monster.mustBeAdult && tame.growth_stage !== tame_growth.adult) { return 'Only fully grown tames can kill this monster.'; @@ -1473,6 +1496,149 @@ async function tameUnequipCommand(user: MUser, itemName: string) { return `You unequipped a ${equippable.item.name} from your ${tameName(tame)}.`; } +export function determineTameClueResult({ + tameGrowthLevel, + clueTier, + extraTripLength, + supportLevel, + equippedArmor, + equippedPrimary +}: { + extraTripLength: number; + clueTier: ClueTier; + tameGrowthLevel: number; + supportLevel: number; + equippedArmor: number | null; + equippedPrimary: number | null; +}) { + const boosts: string[] = []; + let maxTripLength = Time.Minute * 20 * (4 - tameGrowthLevel); + if ( + equippedArmor && + resolveItems(['Abyssal jibwings (e)', 'Demonic jibwings (e)', '3rd age jibwings (e)']).includes(equippedArmor) + ) { + maxTripLength += Time.Minute * 30; + boosts.push('+30mins trip length for enhanced jibwings'); + } + + maxTripLength += extraTripLength; + + let timePerClue = clueTier.timeToFinish * 1.3; + + const s = exponentialPercentScale(supportLevel, 0.03); + const base = exponentialPercentScale(50, 0.03); + const boostPercent = Math.max(0, s / 1.5 - base / 1.5); + + boosts.push(`${boostPercent.toFixed(2)}% faster for support level`); + + if (equippedPrimary === itemID('Divine ring')) { + boosts.push(`20% faster (${formatDuration(calcPercentOfNum(20, timePerClue))} per clue) for Divine ring`); + timePerClue = reduceNumByPercent(timePerClue, 15); + } + + const quantity = Math.floor(maxTripLength / timePerClue); + const duration = Math.floor(quantity * timePerClue); + + const baseCost = (ClueTiers.indexOf(clueTier) + 1) * quantity; + const kibbleNeeded = Math.ceil(baseCost / 1.5); + const cost = new Bank().add('Extraordinary kibble', kibbleNeeded).add(clueTier.scrollID, quantity); + + let costSavedByDemonicJibwings = null; + if (equippedArmor && getSimilarItems(itemID('Demonic jibwings')).includes(equippedArmor) && percentChance(30)) { + costSavedByDemonicJibwings = new Bank().add('Extraordinary kibble', cost.amount('Extraordinary kibble')); + cost.remove(costSavedByDemonicJibwings); + boosts.push('No food used due to demonic jibwings'); + } + + return { + boosts, + quantity, + duration, + cost, + costSavedByDemonicJibwings + }; +} + +async function tameClueCommand(user: MUser, channelID: string, inputName: string) { + const { tame, activity } = await user.fetchActiveTame(); + if (activity) { + return `${tame} is busy.`; + } + if (!tame) { + return 'You have no selected tame.'; + } + if (tame.species.id !== TameSpeciesID.Eagle) { + return `Only Eagle tames can do clue scrolls, switch to a different tame: ${mentionCommand( + globalClient, + 'tames', + 'select' + )}.`; + } + + const clueTier = ClueTiers.find(c => stringMatches(c.name, inputName)); + if (!clueTier) { + return 'Invalid clue tier.'; + } + + let { cost, quantity, duration, boosts, costSavedByDemonicJibwings } = determineTameClueResult({ + tameGrowthLevel: tame.growthLevel, + clueTier, + extraTripLength: patronMaxTripBonus(user) * 2, + supportLevel: tame.currentSupportLevel, + equippedArmor: tame.equippedArmor?.id ?? null, + equippedPrimary: tame.equippedPrimary?.id ?? null + }); + + if (quantity === 0) { + return "Your tame can't do this clue scroll fast enough."; + } + + assert(quantity >= 1 && Number.isInteger(quantity), `${quantity} quantity should be an integer.`); + assert(duration >= 1 && Number.isInteger(duration), `${duration} duration should be an integer.`); + + const units = await user.fetchStashUnits(); + if (units.filter(u => u.tier.tier === clueTier.name).some(u => !u.isFull)) { + return `You need to have all your ${clueTier.name} STASH units built and full.`; + } + if (clueTier.name === 'Grandmaster' && units.some(u => !u.isFull)) { + return 'You need to have all your STASH units built and full for your tame to do Grandmaster clues.'; + } + + if (!user.owns(cost)) { + return `You need ${cost} to feed your Eagle for this trip.`; + } + + await user.removeItemsFromBank(cost); + await tame.addToStatsBank('total_cost', cost); + await updateBankSetting('economyStats_PVMCost', cost); + if (costSavedByDemonicJibwings) { + await tame.addToStatsBank('demonic_jibwings_saved_cost', costSavedByDemonicJibwings); + } + + const task = await createTameTask({ + user, + channelID, + selectedTame: tame.tame, + data: { + type: 'Clues', + clueID: clueTier.scrollID, + quantity + }, + type: 'Clues', + duration, + fakeDuration: undefined + }); + + let reply = `${tame} is now completing ${quantity}x ${itemNameFromID( + clueTier.scrollID + )}. Removed ${cost} from your bank. The trip will take ${formatDuration(task.duration)}.`; + + if (boosts.length > 0) { + reply += `\n\n**Boosts:** ${boosts.join(', ')}.`; + } + + return reply; +} export type TamesCommandOptions = CommandRunOptions<{ set_name?: { name: string }; cancel?: {}; @@ -1495,6 +1661,9 @@ export type TamesCommandOptions = CommandRunOptions<{ activity?: { name: string; }; + clue?: { + clue: string; + }; }>; export const tamesCommand: OSBMahojiCommand = { name: 'tames', @@ -1725,6 +1894,27 @@ export const tamesCommand: OSBMahojiCommand = { } } ] + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'clue', + description: 'Send your eagle tame to do some clue scrolls.', + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'clue', + description: 'The clue tier to do.', + required: true, + autocomplete: async (input, rawUser) => { + const user = await mUserFetch(rawUser.id); + return ClueTiers.filter(t => + !input ? true : t.name.toLowerCase().includes(input.toLowerCase()) + ) + .filter(t => user.bank.has(t.scrollID)) + .map(t => ({ name: `${t.name} (${user.bank.amount(t.scrollID)}x owned)`, value: t.name })); + } + } + ] } ], run: async ({ options, userID, channelID, interaction }: TamesCommandOptions) => { @@ -1745,6 +1935,9 @@ export const tamesCommand: OSBMahojiCommand = { if (options.cast?.spin_flax) return spinFlaxCommand(user, channelID); if (options.cast?.tan) return tanLeatherCommand(user, channelID, options.cast.tan); if (options.cast?.superglass_make) return superGlassCommand(user, channelID); + if (options.clue?.clue) { + return tameClueCommand(user, channelID, options.clue.clue); + } if (options.activity) { const tameActivity = arbitraryTameActivities.find(i => stringMatches(i.name, options.activity!.name)); if (!tameActivity) { diff --git a/src/mahoji/commands/testpotato.ts b/src/mahoji/commands/testpotato.ts index 986e869646..137d39d218 100644 --- a/src/mahoji/commands/testpotato.ts +++ b/src/mahoji/commands/testpotato.ts @@ -10,7 +10,7 @@ import { production } from '../../config'; import { BathhouseOres, BathwaterMixtures } from '../../lib/baxtorianBathhouses'; import { allStashUnitsFlat, allStashUnitTiers } from '../../lib/clues/stashUnits'; import { CombatAchievements } from '../../lib/combat_achievements/combatAchievements'; -import { BitField, MAX_INT_JAVA } from '../../lib/constants'; +import { BitField, BitFieldData, MAX_INT_JAVA } from '../../lib/constants'; import { gorajanArcherOutfit, gorajanOccultOutfit, @@ -23,6 +23,7 @@ import { leaguesCreatables } from '../../lib/data/creatables/leagueCreatables'; import { Eatables } from '../../lib/data/eatables'; import { TOBMaxMageGear, TOBMaxMeleeGear, TOBMaxRangeGear } from '../../lib/data/tob'; import { dyedItems } from '../../lib/dyedItems'; +import { GearSetupType, GearStat } from '../../lib/gear'; import { materialTypes } from '../../lib/invention'; import { DisassemblySourceGroups } from '../../lib/invention/groups'; import { Inventions, transactMaterialsFromUser } from '../../lib/invention/inventions'; @@ -38,7 +39,7 @@ import { prisma } from '../../lib/settings/prisma'; import { getFarmingInfo } from '../../lib/skilling/functions/getFarmingInfo'; import Skills from '../../lib/skilling/skills'; import Farming from '../../lib/skilling/skills/farming'; -import { getUsersTame, tameSpecies } from '../../lib/tames'; +import { tameSpecies } from '../../lib/tames'; import { stringMatches } from '../../lib/util'; import { calcDropRatesFromBankWithoutUniques } from '../../lib/util/calcDropRatesFromBank'; import { @@ -47,15 +48,18 @@ import { getFarmingKeyFromName, userGrowingProgressStr } from '../../lib/util/farmingHelpers'; +import { findBestGearSetups } from '../../lib/util/findBISGear'; import getOSItem from '../../lib/util/getOSItem'; import { deferInteraction } from '../../lib/util/interactionReply'; import { logError } from '../../lib/util/logError'; import { parseStringBank } from '../../lib/util/parseStringBank'; import resolveItems from '../../lib/util/resolveItems'; +import { getUsersTame } from '../../lib/util/tameUtil'; import { getPOH } from '../lib/abstracted_commands/pohCommand'; import { MAX_QP } from '../lib/abstracted_commands/questCommand'; import { allUsableItems } from '../lib/abstracted_commands/useCommand'; import { BingoManager } from '../lib/bingo/BingoManager'; +import { gearSetupOption } from '../lib/mahojiCommandOptions'; import { OSBMahojiCommand } from '../lib/util'; import { userStatsUpdate } from '../mahojiSettings'; import { fetchBingosThatUserIsInvolvedIn } from './bingo'; @@ -65,7 +69,7 @@ import { tameImage } from './tames'; export async function giveMaxStats(user: MUser) { let updates: Prisma.UserUpdateArgs['data'] = {}; for (const skill of Object.values(xp_gains_skill_enum)) { - updates[`skills_${skill}`] = convertLVLtoXP(99); + updates[`skills_${skill}`] = convertLVLtoXP(120); } await user.update({ QP: MAX_QP, @@ -81,7 +85,7 @@ export async function giveMaxStats(user: MUser) { } async function givePatronLevel(user: MUser, tier: number) { - const tierToGive = tiers[tier]; + const tierToGive = tiers[tiers.length - tier]; const currentBitfield = user.bitfield; if (!tier || !tierToGive) { await user.update({ @@ -93,18 +97,20 @@ async function givePatronLevel(user: MUser, tier: number) { await user.update({ bitfield: uniqueArr(newBitField) }); - return `Gave you tier ${tierToGive[1] - 1} patron.`; + return `Gave you ${BitFieldData[tierToGive[1]].name}.`; } const gearPresets = [ { name: 'ToB', - melee: TOBMaxMeleeGear, - mage: TOBMaxMageGear, - range: TOBMaxRangeGear + gear: TOBMaxRangeGear } ]; +for (const stat of Object.values(GearStat)) { + gearPresets.push({ name: `BIS ${stat}`, gear: findBestGearSetups(stat)[0] }); +} + const thingsToReset = [ { name: 'Everything/All', @@ -365,7 +371,13 @@ export const testPotatoCommand: OSBMahojiCommand | null = production type: ApplicationCommandOptionType.String, name: 'preset', description: 'Choose from some preset things to spawn.', - choices: spawnPresets.map(i => ({ name: i[0], value: i[0] })) + autocomplete: async value => { + return spawnPresets + .filter(preset => + !value ? true : preset[0].toLowerCase().includes(value.toLowerCase()) + ) + .map(i => ({ name: i[0], value: i[0] })); + } }, { type: ApplicationCommandOptionType.Boolean, @@ -474,12 +486,22 @@ export const testPotatoCommand: OSBMahojiCommand | null = production name: 'gear', description: 'Spawn and equip gear for a particular thing', options: [ + { + ...gearSetupOption, + required: true + }, { type: ApplicationCommandOptionType.String, - name: 'thing', - description: 'The thing to spawn gear for.', + name: 'preset', + description: 'The preset to spawn and equip.', required: true, - choices: gearPresets.map(i => ({ name: i.name, value: i.name })) + autocomplete: async value => { + return gearPresets + .filter(preset => + !value ? true : preset.name.toLowerCase().includes(value.toLowerCase()) + ) + .map(i => ({ name: i.name, value: i.name })); + } } ] }, @@ -671,7 +693,7 @@ export const testPotatoCommand: OSBMahojiCommand | null = production }: CommandRunOptions<{ max?: {}; patron?: { tier: string }; - gear?: { thing: string }; + gear?: { gear_setup: GearSetupType; preset: string }; reset?: { thing: string }; setminigamekc?: { minigame: string; kc: number }; setxp?: { skill: string; xp: number }; @@ -965,11 +987,9 @@ ${droprates.join('\n')}`), return givePatronLevel(user, Number(options.patron.tier)); } if (options.gear) { - const gear = gearPresets.find(i => stringMatches(i.name, options.gear?.thing))!; + const gear = gearPresets.find(i => stringMatches(i.name, options.gear!.preset))!; await user.update({ - gear_melee: gear.melee.raw() as any, - gear_range: gear.range.raw() as any, - gear_mage: gear.mage.raw() as any + [`gear_${options.gear.gear_setup}`]: gear.gear as any }); return `Set your gear for ${gear.name}.`; } diff --git a/src/mahoji/lib/abstracted_commands/minionKill.ts b/src/mahoji/lib/abstracted_commands/minionKill.ts index d3bba03153..553908f851 100644 --- a/src/mahoji/lib/abstracted_commands/minionKill.ts +++ b/src/mahoji/lib/abstracted_commands/minionKill.ts @@ -5,6 +5,7 @@ import { calcPercentOfNum, calcWhatPercent, increaseNumByPercent, + notEmpty, objectKeys, reduceNumByPercent, round, @@ -22,7 +23,8 @@ import { Eatables } from '../../../lib/data/eatables'; import { getSimilarItems } from '../../../lib/data/similarItems'; import { checkUserCanUseDegradeableItem, degradeablePvmBoostItems, degradeItem } from '../../../lib/degradeableItems'; import { Diary, DiaryTier, userhasDiaryTier } from '../../../lib/diaries'; -import { GearSetupType } from '../../../lib/gear/types'; +import { readableStatName } from '../../../lib/gear'; +import { GearSetupType, GearStat } from '../../../lib/gear/types'; import { canAffordInventionBoost, InventionID, inventionItemBoost } from '../../../lib/invention/inventions'; import { trackLoot } from '../../../lib/lootTrack'; import { @@ -56,7 +58,7 @@ import { calcPOHBoosts } from '../../../lib/poh'; import { SkillsEnum } from '../../../lib/skilling/types'; import { SlayerTaskUnlocksEnum } from '../../../lib/slayer/slayerUnlocks'; import { determineBoostChoice, getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; -import { maxOffenceStats } from '../../../lib/structures/Gear'; +import { addStatsOfItemsTogether, maxOffenceStats } from '../../../lib/structures/Gear'; import { Peak } from '../../../lib/tickers'; import { MonsterActivityTaskOptions } from '../../../lib/types/minions'; import { @@ -261,7 +263,26 @@ export async function minionKillCommand( } } - let [timeToFinish, percentReduced] = reducedTimeFromKC(monster, await user.getKC(monster.id)); + if (monster.minimumWeaponShieldStats) { + for (const [setup, minimum] of Object.entries(monster.minimumWeaponShieldStats)) { + const gear = user.gear[setup as GearSetupType]; + const stats = addStatsOfItemsTogether( + [gear['2h']?.item, gear.weapon?.item, gear.shield?.item].filter(notEmpty) + ); + for (const [key, requiredValue] of Object.entries(minimum)) { + if (requiredValue < 1) continue; + const theirValue = stats[key as GearStat] ?? 0; + if (theirValue < requiredValue) { + return `Your ${setup} weapons/shield need to have at least ${requiredValue} ${readableStatName( + key + )} to kill ${monster.name}, you have ${theirValue}.`; + } + } + } + } + + const kcForThisMonster = await user.getKC(monster.id); + let [timeToFinish, percentReduced] = reducedTimeFromKC(monster, kcForThisMonster); const [, osjsMon, attackStyles] = resolveAttackStyles(user, { monsterID: monster.id, @@ -606,7 +627,8 @@ export async function minionKillCommand( for (const degItem of degradeablePvmBoostItems) { const isUsing = convertPvmStylesToGearSetup(attackStyles).includes(degItem.attackStyle) && - user.gear[degItem.attackStyle].hasEquipped(degItem.item.id); + user.gear[degItem.attackStyle].hasEquipped(degItem.item.id) && + (monster.setupsUsed ? monster.setupsUsed.includes(degItem.attackStyle) : true); if (isUsing) { // We assume they have enough charges, add the boost, and degrade at the end to avoid doing it twice. degItemBeingUsed.push(degItem); @@ -638,6 +660,9 @@ export async function minionKillCommand( quantity = floor(maxTripLength / timeToFinish); } } + + quantity = Math.max(1, quantity); + if (isOnTask) { let effectiveQtyRemaining = usersTask.currentTask!.quantity_remaining; if ( @@ -673,6 +698,12 @@ export async function minionKillCommand( } } + if (monster.customRequirement && kcForThisMonster === 0) { + const reasonDoesntHaveReq = await monster.customRequirement(user); + if (reasonDoesntHaveReq) { + return `You don't meet the requirements to kill this monster: ${reasonDoesntHaveReq}.`; + } + } if (monster.requiredBitfield && !user.bitfield.includes(monster.requiredBitfield)) { return "You haven't unlocked this monster.."; } @@ -869,6 +900,9 @@ export async function minionKillCommand( ' eyes with your minion and grabs the dart mid-air, and throws it back, killing your minion instantly.' ); } + if (monster.name === 'Solis') { + return 'The dart melts into a crisp dust before coming into contact with Solis.'; + } if (monster.name === 'Yeti') { return 'You send your minion off to fight Yeti with a Deathtouched dart, they stand a safe distance and throw the dart - the cold, harsh wind blows it out of the air. Your minion runs back to you in fear.'; } @@ -974,7 +1008,8 @@ export async function minionKillCommand( ? ['wildy'] : uniqueArr([...objectKeys(monster.minimumGearRequirements ?? {}), gearToCheck]), learningPercentage: percentReduced, - isWilderness: monster.wildy + isWilderness: monster.wildy, + minimumHealAmount: monster.minimumFoodHealAmount }); if (foodRemoved.length === 0) { @@ -1021,6 +1056,11 @@ export async function minionKillCommand( duration = reduceNumByPercent(duration, noFoodBoost); } + if (monster.deathProps) { + const deathChance = calculateSimpleMonsterDeathChance({ ...monster.deathProps, currentKC: kcForThisMonster }); + messages.push(`${deathChance.toFixed(1)}% chance of death`); + } + // 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); diff --git a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts index 6710ad8e62..ac40b7bd1c 100644 --- a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts +++ b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts @@ -8,7 +8,6 @@ import { getUsersFishingContestDetails } from '../../../lib/fishingContest'; import { clArrayUpdate } from '../../../lib/handleNewCLItems'; import { roboChimpSyncData, roboChimpUserFetch } from '../../../lib/roboChimp'; import { prisma } from '../../../lib/settings/prisma'; -import { getUsersTame, shortTameTripDesc, tameLastFinishedActivity } from '../../../lib/tames'; import { makeComponents } from '../../../lib/util'; import { makeAutoContractButton, @@ -17,6 +16,7 @@ import { } from '../../../lib/util/globalInteractions'; import { minionStatus } from '../../../lib/util/minionStatus'; import { makeRepeatTripButtons } from '../../../lib/util/repeatStoredTrip'; +import { getUsersTame, shortTameTripDesc, tameLastFinishedActivity } from '../../../lib/util/tameUtil'; import { getItemContractDetails } from '../../commands/ic'; import { spawnLampIsReady } from '../../commands/tools'; import { calculateBirdhouseDetails } from './birdhousesCommand'; diff --git a/src/mahoji/lib/inhibitors.ts b/src/mahoji/lib/inhibitors.ts index a3d6f3e262..118bc2a904 100644 --- a/src/mahoji/lib/inhibitors.ts +++ b/src/mahoji/lib/inhibitors.ts @@ -8,10 +8,19 @@ import { TextChannel, User } from 'discord.js'; +import { roll } from 'e'; import { OWNER_IDS, SupportServer } from '../../config'; import { BLACKLISTED_GUILDS, BLACKLISTED_USERS } from '../../lib/blacklists'; -import { BadgesEnum, BitField, Channel, DISABLED_COMMANDS, minionBuyButton, PerkTier } from '../../lib/constants'; +import { + BadgesEnum, + BitField, + Channel, + DISABLED_COMMANDS, + gearValidationChecks, + minionBuyButton, + PerkTier +} from '../../lib/constants'; import { perkTierCache, syncPerkTierOfUser } from '../../lib/perkTiers'; import { CategoryFlag } from '../../lib/types'; import { formatDuration } from '../../lib/util'; @@ -263,6 +272,18 @@ export async function runInhibitors({ guild: Guild | null; bypassInhibitors: boolean; }): Promise { + if (!gearValidationChecks.has(user.id) && roll(3)) { + const { itemsUnequippedAndRefunded } = await user.validateEquippedGear(); + if (itemsUnequippedAndRefunded.length > 0) { + return { + reason: { + content: `You had some items equipped that you didn't have the requirements to use, so they were unequipped and refunded to your bank: ${itemsUnequippedAndRefunded}` + }, + silent: false + }; + } + } + for (const { run, canBeDisabled, silent } of inhibitors) { if (bypassInhibitors && canBeDisabled) continue; const result = await run({ user, channel, member, command, guild, APIUser }); diff --git a/src/mahoji/mahojiSettings.ts b/src/mahoji/mahojiSettings.ts index 7c2d2f28f2..475a17e5ad 100644 --- a/src/mahoji/mahojiSettings.ts +++ b/src/mahoji/mahojiSettings.ts @@ -4,6 +4,7 @@ import { isFunction, objectEntries, round } from 'e'; import { Bank } from 'oldschooljs'; import { globalConfig } from '../lib/constants'; +import { getSimilarItems } from '../lib/data/similarItems'; import type { KillableMonster } from '../lib/minions/types'; import type { SelectedUserStats } from '../lib/MUser'; import { prisma } from '../lib/settings/prisma'; @@ -254,7 +255,7 @@ export function hasMonsterRequirements(user: MUser, monster: KillableMonster) { if (!item.some(itemReq => user.hasEquippedOrInBank(itemReq as number))) { return [false, `You need these items to kill ${monster.name}: ${itemsRequiredStr}`]; } - } else if (!user.hasEquippedOrInBank(item)) { + } else if (!getSimilarItems(item).some(id => user.hasEquippedOrInBank(id))) { return [ false, `You need ${itemsRequiredStr} to kill ${monster.name}. You're missing ${itemNameFromID(item)}.` diff --git a/src/tasks/minions/bso/bonanzaActivity.ts b/src/tasks/minions/bso/bonanzaActivity.ts index 67494186da..2972ab87ef 100644 --- a/src/tasks/minions/bso/bonanzaActivity.ts +++ b/src/tasks/minions/bso/bonanzaActivity.ts @@ -5,9 +5,10 @@ import { MAX_LEVEL } from '../../../lib/constants'; import { spectatorClothes } from '../../../lib/data/CollectionsExport'; import { incrementMinigameScore } from '../../../lib/settings/settings'; import { SkillsEnum } from '../../../lib/skilling/types'; -import { getAllUserTames, tameName, TameSpeciesID } from '../../../lib/tames'; +import { getAllUserTames, TameSpeciesID } from '../../../lib/tames'; import { MinigameActivityTaskOptionsWithNoChanges } from '../../../lib/types/minions'; import { handleTripFinish } from '../../../lib/util/handleTripFinish'; +import { tameName } from '../../../lib/util/tameUtil'; function calcXP(user: MUser, duration: number, skill: SkillsEnum) { return calcPercentOfNum(calcWhatPercent(user.skillLevel(skill), MAX_LEVEL), duration / 80); diff --git a/src/tasks/minions/bso/memoryHarvestActivity.ts b/src/tasks/minions/bso/memoryHarvestActivity.ts index e21daf9494..ec4dc8749d 100644 --- a/src/tasks/minions/bso/memoryHarvestActivity.ts +++ b/src/tasks/minions/bso/memoryHarvestActivity.ts @@ -1,7 +1,12 @@ import { calcPercentOfNum, increaseNumByPercent } from 'e'; import { Bank } from 'oldschooljs'; -import { divinationEnergies, MemoryHarvestType } from '../../../lib/bso/divination'; +import { + calcEnergyPerMemory, + divinationEnergies, + DivinationEnergy, + MemoryHarvestType +} from '../../../lib/bso/divination'; import { Emoji } from '../../../lib/constants'; import { inventionBoosts } from '../../../lib/invention/inventions'; import { SkillsEnum } from '../../../lib/skilling/types'; @@ -16,7 +21,7 @@ const MEMORIES_PER_HARVEST = SECONDS_TO_HARVEST * 2; export const totalTimePerRound = SECONDS_TO_HARVEST + SECONDS_TO_CONVERT * MEMORIES_PER_HARVEST; -function calcConversionResult(hasBoon: boolean, method: MemoryHarvestType, energy: (typeof divinationEnergies)[0]) { +function calcConversionResult(hasBoon: boolean, method: MemoryHarvestType, energy: DivinationEnergy) { let convertToXPXP = hasBoon ? energy.convertBoon ?? energy.convertNormal : energy.convertNormal; switch (method) { @@ -28,7 +33,9 @@ function calcConversionResult(hasBoon: boolean, method: MemoryHarvestType, energ return { xp }; } case MemoryHarvestType.ConvertWithEnergyToXP: { - let xp = hasBoon ? energy.convertWithEnergyAndBoon ?? energy.convertWithEnergy : energy.convertWithEnergy; + let xp: number = hasBoon + ? energy.convertWithEnergyAndBoon ?? energy.convertWithEnergy + : energy.convertWithEnergy; xp = increaseNumByPercent(xp, 15); return { xp }; } @@ -51,7 +58,7 @@ export function memoryHarvestResult({ rounds }: { duration: number; - energy: (typeof divinationEnergies)[0]; + energy: DivinationEnergy; harvestMethod: MemoryHarvestType; hasBoon: boolean; hasWispBuster: boolean; @@ -69,7 +76,7 @@ export function memoryHarvestResult({ const cost = new Bank(); let totalDivinationXP = 0; let totalMemoriesHarvested = 0; - const energyPerMemory = (120 - energy.level) / 150; + const energyPerMemory = calcEnergyPerMemory(energy); for (let i = 0; i < rounds; i++) { // Step 1: Harvest memories diff --git a/src/tasks/minions/monsterActivity.ts b/src/tasks/minions/monsterActivity.ts index 5eaa17fe94..e8fa13791c 100644 --- a/src/tasks/minions/monsterActivity.ts +++ b/src/tasks/minions/monsterActivity.ts @@ -1,5 +1,14 @@ import { Prisma } from '@prisma/client'; -import { calcWhatPercent, deepClone, increaseNumByPercent, percentChance, reduceNumByPercent, sumArr, Time } from 'e'; +import { + calcWhatPercent, + deepClone, + increaseNumByPercent, + percentChance, + randArrItem, + reduceNumByPercent, + sumArr, + Time +} from 'e'; import { Bank, MonsterKillOptions, Monsters } from 'oldschooljs'; import { MonsterAttribute } from 'oldschooljs/dist/meta/monsterData'; import { ItemBank } from 'oldschooljs/dist/meta/types'; @@ -402,27 +411,39 @@ export const monsterTask: MinionTask = { } const xpRes: string[] = []; - xpRes.push( - await addMonsterXP(user, { - monsterID, - quantity, - duration, - isOnTask: isOnTaskResult.isOnTask, - taskQuantity: isOnTaskResult.isOnTask ? isOnTaskResult.quantitySlayed : null, - minimal: true, - usingCannon, - cannonMulti, - burstOrBarrage, - superiorCount: newSuperiorCount - }) - ); + if (quantity >= 1) { + xpRes.push( + await addMonsterXP(user, { + monsterID, + quantity, + duration, + isOnTask: isOnTaskResult.isOnTask, + taskQuantity: isOnTaskResult.isOnTask ? isOnTaskResult.quantitySlayed : null, + minimal: true, + usingCannon, + cannonMulti, + burstOrBarrage, + superiorCount: newSuperiorCount + }) + ); + } if (hasKourendHard) await ashSanctifierEffect(user, loot, duration, xpRes); const superiorMessage = newSuperiorCount ? `, including **${newSuperiorCount} superiors**` : ''; + const sorryMessages = [ + 'They apologized for dying so much.', + "They're sorry for dying so much.", + "They're sorry.", + 'They said they will do better.' + ]; let str = - `${user}, ${user.minionName} finished killing ${quantity} ${monster.name}${superiorMessage}.` + - ` Your ${monster.name} KC is now ${newKC}.\n${xpRes}\n`; + quantity === 0 + ? `${user}, ${user.minionName} died in ALL their kill attempts!${ + roll(10) ? ` ${randArrItem(sorryMessages)}` : '' + }` + : `${user}, ${user.minionName} finished killing ${quantity} ${monster.name}${superiorMessage}.` + + ` Your ${monster.name} KC is now ${newKC}.\n${xpRes}\n`; if (masterCapeRolls > 0) { messages.push(`${Emoji.SlayerMasterCape} You received ${masterCapeRolls}x bonus superior rolls`); diff --git a/src/tasks/tames/tameTasks.ts b/src/tasks/tames/tameTasks.ts new file mode 100644 index 0000000000..f83d56f900 --- /dev/null +++ b/src/tasks/tames/tameTasks.ts @@ -0,0 +1,508 @@ +import { channelIsSendable } from '@oldschoolgg/toolkit'; +import { Tame, TameActivity } from '@prisma/client'; +import { + ActionRowBuilder, + APIInteractionGuildMember, + AttachmentBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + ChatInputCommandInteraction, + GuildMember, + userMention +} from 'discord.js'; +import { increaseNumByPercent, isFunction, percentChance, randArrItem, randInt, roll, Time } from 'e'; +import { isEmpty } from 'lodash'; +import { Bank } from 'oldschooljs'; +import { ItemBank } from 'oldschooljs/dist/meta/types'; + +import { ClueTiers } from '../../lib/clues/clueTiers'; +import { BitField } from '../../lib/constants'; +import { handlePassiveImplings } from '../../lib/implings'; +import { trackLoot } from '../../lib/lootTrack'; +import { allOpenables } from '../../lib/openables'; +import { prisma } from '../../lib/settings/prisma'; +import { runCommand } from '../../lib/settings/settings'; +import { getTemporossLoot } from '../../lib/simulation/tempoross'; +import { WintertodtCrate } from '../../lib/simulation/wintertodt'; +import { MTame } from '../../lib/structures/MTame'; +import { + addDurationToTame, + ArbitraryTameActivity, + seaMonkeySpells, + tameKillableMonsters, + TameSpeciesID, + TameTaskOptions, + TameType +} from '../../lib/tames'; +import { ActivityTaskData } from '../../lib/types/minions'; +import { assert, calcPerHour, formatDuration, itemNameFromID } from '../../lib/util'; +import getOSItem from '../../lib/util/getOSItem'; +import { getUsersTamesCollectionLog } from '../../lib/util/getUsersTameCL'; +import { makeBankImage } from '../../lib/util/makeBankImage'; +import { tameHasBeenFed, tameLastFinishedActivity, tameName } from '../../lib/util/tameUtil'; +import { collectables } from '../../mahoji/lib/abstracted_commands/collectCommand'; + +export const arbitraryTameActivities: ArbitraryTameActivity[] = [ + { + name: 'Tempoross', + id: 'Tempoross', + allowedTames: [TameSpeciesID.Monkey], + run: async ({ handleFinish, user, duration, tame }) => { + const quantity = Math.ceil(duration / (Time.Minute * 5)); + const loot = getTemporossLoot( + quantity, + tame.max_gatherer_level + 15, + await getUsersTamesCollectionLog(user.id) + ); + const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); + handleFinish({ + loot: itemsAdded, + message: `${user}, ${tameName( + tame + )} finished defeating Tempoross ${quantity}x times, and received ${loot}.`, + user + }); + } + }, + { + name: 'Wintertodt', + id: 'Wintertodt', + allowedTames: [TameSpeciesID.Igne], + run: async ({ handleFinish, user, duration, tame }) => { + const quantity = Math.ceil(duration / (Time.Minute * 5)); + const loot = new Bank(); + for (let i = 0; i < quantity; i++) { + loot.add( + WintertodtCrate.open({ + points: randArrItem([500, 500, 750, 1000]), + itemsOwned: user.bank.bank, + skills: user.skillsAsXP, + firemakingXP: user.skillsAsXP.firemaking + }) + ); + } + const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); + handleFinish({ + loot: itemsAdded, + message: `${user}, ${tameName( + tame + )} finished defeating Wintertodt ${quantity}x times, and received ${loot}.`, + user + }); + } + } +]; + +function doubleLootCheck(tame: Tame, loot: Bank) { + const hasMrE = tameHasBeenFed(tame, 'Mr. E'); + let doubleLootMsg = ''; + if (hasMrE && roll(12)) { + loot.multiply(2); + doubleLootMsg = '\n**2x Loot from Mr. E**'; + } + + return { loot, doubleLootMsg }; +} + +export async function runTameTask(activity: TameActivity, tame: Tame) { + async function handleFinish(res: { loot: Bank | null; message: string; user: MUser }) { + const previousTameCl = new Bank({ ...(tame.max_total_loot as ItemBank) }); + + if (res.loot) { + await prisma.tame.update({ + where: { + id: tame.id + }, + data: { + max_total_loot: previousTameCl.clone().add(res.loot.bank).bank, + last_activity_date: new Date() + } + }); + } + const addRes = await addDurationToTame(tame, activity.duration); + if (addRes) res.message += `\n${addRes}`; + + const channel = globalClient.channels.cache.get(activity.channel_id); + if (!channelIsSendable(channel)) return; + channel.send({ + content: res.message, + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('REPEAT_TAME_TRIP') + .setLabel('Repeat Trip') + .setStyle(ButtonStyle.Secondary) + ) + ], + files: res.loot + ? [ + new AttachmentBuilder( + ( + await makeBankImage({ + bank: res.loot, + title: `${tameName(tame)}'s Loot`, + user: res.user, + previousCL: previousTameCl + }) + ).file.attachment + ) + ] + : undefined + }); + } + const user = await mUserFetch(activity.user_id); + + const activityData = activity.data as any as TameTaskOptions; + switch (activityData.type) { + case 'pvm': { + const { quantity, monsterID } = activityData; + const mon = tameKillableMonsters.find(i => i.id === monsterID)!; + + let killQty = quantity - activity.deaths; + if (killQty < 1) { + handleFinish({ + loot: null, + message: `${userMention(user.id)}, Your tame died in all their attempts to kill ${ + mon.name + }. Get them some better armor!`, + user + }); + return; + } + const hasOri = tameHasBeenFed(tame, 'Ori'); + + const oriIsApplying = hasOri && mon.oriWorks !== false; + // If less than 8 kills, roll 25% chance per kill + if (oriIsApplying) { + if (killQty >= 8) { + killQty = Math.ceil(increaseNumByPercent(killQty, 25)); + } else { + for (let i = 0; i < quantity; i++) { + if (roll(4)) killQty++; + } + } + } + const loot = mon.loot({ quantity: killQty, tame }); + let str = `${user}, ${tameName(tame)} finished killing ${quantity}x ${mon.name}.${ + activity.deaths > 0 ? ` ${tameName(tame)} died ${activity.deaths}x times.` : '' + }`; + const boosts = []; + if (oriIsApplying) { + boosts.push('25% extra loot (ate an Ori)'); + } + if (boosts.length > 0) { + str += `\n\n**Boosts:** ${boosts.join(', ')}.`; + } + const { doubleLootMsg } = doubleLootCheck(tame, loot); + str += doubleLootMsg; + const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); + await trackLoot({ + duration: activity.duration, + kc: activityData.quantity, + id: mon.name, + changeType: 'loot', + type: 'Monster', + totalLoot: loot, + suffix: 'tame', + users: [ + { + id: user.id, + loot: itemsAdded, + duration: activity.duration + } + ] + }); + handleFinish({ + loot: itemsAdded, + message: str, + user + }); + break; + } + case 'collect': { + const { quantity, itemID } = activityData; + const collectable = collectables.find(c => c.item.id === itemID)!; + const totalQuantity = quantity * collectable.quantity; + const loot = new Bank().add(collectable.item.id, totalQuantity); + let str = `${user}, ${tameName(tame)} finished collecting ${totalQuantity}x ${ + collectable.item.name + }. (${Math.round((totalQuantity / (activity.duration / Time.Minute)) * 60).toLocaleString()}/hr)`; + const { doubleLootMsg } = doubleLootCheck(tame, loot); + str += doubleLootMsg; + const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); + handleFinish({ + loot: itemsAdded, + message: str, + user + }); + break; + } + case 'SpellCasting': { + const spell = seaMonkeySpells.find(s => s.id === activityData.spellID)!; + const loot = new Bank(activityData.loot); + let str = `${user}, ${tameName(tame)} finished casting the ${spell.name} spell for ${formatDuration( + activity.duration + )}. ${loot + .items() + .map(([item, qty]) => `${Math.floor(calcPerHour(qty, activity.duration)).toFixed(1)}/hr ${item.name}`) + .join(', ')}`; + const { doubleLootMsg } = doubleLootCheck(tame, loot); + str += doubleLootMsg; + const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); + handleFinish({ + loot: itemsAdded, + message: str, + user + }); + break; + } + case 'Clues': { + const mTame = new MTame(tame); + const clueTier = ClueTiers.find(c => c.scrollID === activityData.clueID)!; + const messages: string[] = []; + const loot = new Bank(); + + let actualOpenQuantityWithBonus = 0; + for (let i = 0; i < activityData.quantity; i++) { + actualOpenQuantityWithBonus += randInt(1, 3); + } + + if (clueTier.name === 'Master') { + const percentChanceOfGMC = mTame.hasEquipped('Divine ring') ? 3.5 : 1.5; + for (let i = 0; i < activityData.quantity; i++) { + if (percentChance(percentChanceOfGMC)) { + loot.add('Clue scroll (grandmaster)'); + } + } + messages.push('2x GMC droprate for divine ring'); + } + + if (user.bitfield.includes(BitField.DisabledTameClueOpening)) { + loot.add(clueTier.id, activityData.quantity); + } else { + const openingLoot = clueTier.table.open(actualOpenQuantityWithBonus); + + if (mTame.hasEquipped('Abyssal jibwings') && clueTier !== ClueTiers[0]) { + const lowerTier = ClueTiers[ClueTiers.indexOf(clueTier) - 1]; + const abysJwLoot = new Bank(); + let bonusClues = 0; + for (let i = 0; i < activityData.quantity; i++) { + if (percentChance(5)) { + bonusClues++; + abysJwLoot.add(lowerTier.table.open(1)); + } + } + if (abysJwLoot.length > 0) { + loot.add(abysJwLoot); + await mTame.addToStatsBank('abyssal_jibwings_loot', abysJwLoot); + messages.push( + `You received the loot from ${bonusClues}x ${lowerTier.name} caskets from your Abyssal jibwings.` + ); + } + } else if (mTame.hasEquipped('3rd age jibwings') && openingLoot.has('Coins')) { + const thirdAgeJwLoot = new Bank().add('Coins', openingLoot.amount('Coins')); + loot.add(thirdAgeJwLoot); + messages.push(`You received ${thirdAgeJwLoot} from your 3rd age jibwings.`); + await mTame.addToStatsBank('third_age_jibwings_loot', thirdAgeJwLoot); + } + + loot.add(openingLoot); + } + + if (mTame.hasBeenFed('Impling locator')) { + const result = await handlePassiveImplings(user, { + type: 'MonsterKilling', + duration: activity.duration + } as ActivityTaskData); + if (result && result.bank.length > 0) { + const actualImplingLoot = new Bank(); + for (const [item, qty] of result.bank.items()) { + const openable = allOpenables.find(i => i.id === item.id)!; + assert(!isEmpty(openable)); + actualImplingLoot.add( + isFunction(openable.output) + ? ( + await openable.output({ + user, + quantity: qty, + self: openable, + totalLeaguesPoints: 0 + }) + ).bank + : openable.output.roll(qty) + ); + } + loot.add(actualImplingLoot); + messages.push(`${mTame} caught ${result.bank} with their Impling locator!`); + await mTame.addToStatsBank('implings_loot', actualImplingLoot); + } + } + + let str = `${user}, ${mTame} finished completing ${activityData.quantity}x ${itemNameFromID( + clueTier.scrollID + )}. (${Math.floor(calcPerHour(activityData.quantity, activity.duration)).toFixed(1)} clues/hr)`; + + if (messages) { + str += `\n\n${messages.join('\n')}`; + } + + const { itemsAdded } = await user.addItemsToBank({ items: loot, collectionLog: false }); + handleFinish({ + loot: itemsAdded, + message: str, + user + }); + break; + } + case 'Tempoross': + case 'Wintertodt': { + const act = arbitraryTameActivities.find(i => i.id === activityData.type)!; + await act.run({ handleFinish, user, tame, duration: activity.duration }); + break; + } + default: { + console.error('Unmatched tame activity type', activity.type); + break; + } + } +} + +export async function repeatTameTrip({ + channelID, + guildID, + user, + member, + interaction, + continueDeltaMillis +}: { + channelID: string; + guildID: string | null; + user: MUser; + member: APIInteractionGuildMember | GuildMember | null; + interaction: ButtonInteraction | ChatInputCommandInteraction; + continueDeltaMillis: number | null; +}) { + const activity = await tameLastFinishedActivity(user); + if (!activity) { + return; + } + const data = activity.data as unknown as TameTaskOptions; + switch (data.type) { + case TameType.Combat: { + const mon = tameKillableMonsters.find(i => i.id === data.monsterID); + return runCommand({ + commandName: 'tames', + args: { + kill: { + name: mon!.name + } + }, + bypassInhibitors: true, + channelID, + guildID, + user, + member, + interaction, + continueDeltaMillis + }); + } + case TameType.Gatherer: { + return runCommand({ + commandName: 'tames', + args: { + collect: { + name: getOSItem(data.itemID).name + } + }, + bypassInhibitors: true, + channelID, + guildID, + user, + member, + interaction, + continueDeltaMillis + }); + } + case 'SpellCasting': { + let args = {}; + switch (data.spellID) { + case 1: { + args = { + tan: getOSItem(data.itemID).name + }; + break; + } + case 2: { + args = { + plank_make: getOSItem(data.itemID).name + }; + break; + } + case 3: { + args = { + spin_flax: 'flax' + }; + break; + } + case 4: { + args = { + superglass_make: 'molten glass' + }; + break; + } + } + return runCommand({ + commandName: 'tames', + args: { + cast: args + }, + bypassInhibitors: true, + channelID, + guildID, + user, + member, + interaction, + continueDeltaMillis + }); + } + case 'Tempoross': + case 'Wintertodt': { + return runCommand({ + commandName: 'tames', + args: { + activity: { + name: data.type + } + }, + bypassInhibitors: true, + channelID, + guildID, + user, + member, + interaction, + continueDeltaMillis + }); + } + case 'Clues': { + const clueTier = ClueTiers.find(c => c.scrollID === data.clueID)!; + return runCommand({ + commandName: 'tames', + args: { + clue: { + clue: clueTier.name + } + }, + bypassInhibitors: true, + channelID, + guildID, + user, + member, + interaction, + continueDeltaMillis + }); + } + default: { + } + } +} diff --git a/tests/integration/memoryHarvesting.bso.test.ts b/tests/integration/memoryHarvesting.bso.test.ts index 67d2230610..8302541f8d 100644 --- a/tests/integration/memoryHarvesting.bso.test.ts +++ b/tests/integration/memoryHarvesting.bso.test.ts @@ -3,7 +3,6 @@ import { ItemBank } from 'oldschooljs/dist/meta/types'; import { describe, expect, test } from 'vitest'; import { MemoryHarvestType } from '../../src/lib/bso/divination'; -import { prisma } from '../../src/lib/settings/prisma'; import { Gear } from '../../src/lib/structures/Gear'; import { MemoryHarvestOptions } from '../../src/lib/types/minions'; import itemID from '../../src/lib/util/itemID'; @@ -11,13 +10,8 @@ import { divinationCommand } from '../../src/mahoji/commands/divination'; import { createTestUser, mockClient } from './util'; describe('Divination', async () => { - const client = await mockClient(); - await prisma.clientStorage.update({ - where: { id: client.data.id }, - data: { - divination_is_released: true - } - }); + await mockClient(); + test('Memory Harvesting - Convert to XP', async () => { const user = await createTestUser(); const gear = new Gear(); diff --git a/tests/unit/sanity.test.ts b/tests/unit/sanity.test.ts index 4dac59f04f..6457961379 100644 --- a/tests/unit/sanity.test.ts +++ b/tests/unit/sanity.test.ts @@ -1,3 +1,4 @@ +import { Tame, tame_growth } from '@prisma/client'; import { Items, Monsters } from 'oldschooljs'; import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; import { assert, describe, expect, test } from 'vitest'; @@ -20,6 +21,7 @@ import getOSItem from '../../src/lib/util/getOSItem'; import itemID from '../../src/lib/util/itemID'; import itemIsTradeable from '../../src/lib/util/itemIsTradeable'; import resolveItems from '../../src/lib/util/resolveItems'; +import { calculateMaximumTameFeedingLevelGain } from '../../src/lib/util/tameUtil'; import { BingoTrophies } from '../../src/mahoji/lib/bingo/BingoManager'; describe('Sanity', () => { @@ -303,4 +305,13 @@ describe('Sanity', () => { // @ts-ignore ignore expect(getOSItem("Gatherer's cape").equipment!.requirements?.divination).toEqual(120); }); + test('calculateMaximumTameFeedingLevelGain', () => { + expect( + calculateMaximumTameFeedingLevelGain({ + species_id: 1, + max_combat_level: 70, + growth_stage: tame_growth.adult + } as Tame) + ).toEqual(14); + }); }); diff --git a/tests/unit/snapshots/banksnapshots.test.ts.snap b/tests/unit/snapshots/banksnapshots.test.ts.snap index 12326ac245..f1957174ce 100644 --- a/tests/unit/snapshots/banksnapshots.test.ts.snap +++ b/tests/unit/snapshots/banksnapshots.test.ts.snap @@ -25263,6 +25263,44 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73010": 1, + }, + "frozen": false, + }, + "name": "Revert Pale energy", + "outputItems": Bank { + "bank": { + "73090": 2, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 1, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73006": 1, + }, + "frozen": false, + }, + "name": "Revert Flickering energy", + "outputItems": Bank { + "bank": { + "73090": 3, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 10, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25282,6 +25320,25 @@ exports[`BSO Creatables 1`] = ` "divination": 10, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73004": 1, + }, + "frozen": false, + }, + "name": "Revert Bright energy", + "outputItems": Bank { + "bank": { + "73090": 6, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 20, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25301,6 +25358,25 @@ exports[`BSO Creatables 1`] = ` "divination": 20, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73009": 1, + }, + "frozen": false, + }, + "name": "Revert Glowing energy", + "outputItems": Bank { + "bank": { + "73090": 11, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 30, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25320,6 +25396,25 @@ exports[`BSO Creatables 1`] = ` "divination": 30, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73015": 1, + }, + "frozen": false, + }, + "name": "Revert Sparkling energy", + "outputItems": Bank { + "bank": { + "73090": 18, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 40, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25339,6 +25434,25 @@ exports[`BSO Creatables 1`] = ` "divination": 40, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73008": 1, + }, + "frozen": false, + }, + "name": "Revert Gleaming energy", + "outputItems": Bank { + "bank": { + "73090": 28, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 50, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25358,6 +25472,25 @@ exports[`BSO Creatables 1`] = ` "divination": 50, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73014": 1, + }, + "frozen": false, + }, + "name": "Revert Vibrant energy", + "outputItems": Bank { + "bank": { + "73090": 42, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 60, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25377,6 +25510,25 @@ exports[`BSO Creatables 1`] = ` "divination": 60, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73013": 1, + }, + "frozen": false, + }, + "name": "Revert Lustrous energy", + "outputItems": Bank { + "bank": { + "73090": 61, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 70, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25396,6 +25548,25 @@ exports[`BSO Creatables 1`] = ` "divination": 70, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73007": 1, + }, + "frozen": false, + }, + "name": "Revert Elder energy", + "outputItems": Bank { + "bank": { + "73090": 72, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 75, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25415,6 +25586,25 @@ exports[`BSO Creatables 1`] = ` "divination": 75, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73005": 1, + }, + "frozen": false, + }, + "name": "Revert Brilliant energy", + "outputItems": Bank { + "bank": { + "73090": 85, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 80, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25434,6 +25624,25 @@ exports[`BSO Creatables 1`] = ` "divination": 80, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73016": 1, + }, + "frozen": false, + }, + "name": "Revert Radiant energy", + "outputItems": Bank { + "bank": { + "73090": 99, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 85, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25453,6 +25662,25 @@ exports[`BSO Creatables 1`] = ` "divination": 85, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73012": 1, + }, + "frozen": false, + }, + "name": "Revert Luminous energy", + "outputItems": Bank { + "bank": { + "73090": 115, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 90, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25472,6 +25700,25 @@ exports[`BSO Creatables 1`] = ` "divination": 90, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73011": 1, + }, + "frozen": false, + }, + "name": "Revert Incandescent energy", + "outputItems": Bank { + "bank": { + "73090": 133, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 95, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -25491,6 +25738,25 @@ exports[`BSO Creatables 1`] = ` "divination": 95, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73045": 1, + }, + "frozen": false, + }, + "name": "Revert Ancient energy", + "outputItems": Bank { + "bank": { + "73090": 394, + }, + "frozen": false, + }, + "requiredSkills": { + "divination": 110, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -31310,6 +31576,205 @@ exports[`BSO Creatables 1`] = ` "smithing": 120, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "19685": 10, + }, + "frozen": false, + }, + "materialCost": MaterialBank { + "bank": { + "strong": 500, + }, + }, + "name": "Demonic jibwings", + "outputItems": Bank { + "bank": { + "73097": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 90, + "invention": 90, + "smithing": 90, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "19677": 10, + "50024": 1, + }, + "frozen": false, + }, + "materialCost": MaterialBank { + "bank": { + "abyssal": 30, + }, + }, + "name": "Abyssal jibwings", + "outputItems": Bank { + "bank": { + "73098": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 90, + "invention": 90, + "smithing": 90, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "2357": 1000, + }, + "frozen": false, + }, + "materialCost": MaterialBank { + "bank": { + "third-age": 6, + }, + }, + "name": "3rd age jibwings", + "outputItems": Bank { + "bank": { + "73099": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 90, + "invention": 90, + "smithing": 90, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "48207": 1000, + "73099": 1, + }, + "frozen": false, + }, + "name": "3rd age jibwings (e)", + "outputItems": Bank { + "bank": { + "73104": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 105, + "invention": 105, + "smithing": 105, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "48207": 1000, + "73097": 1, + }, + "frozen": false, + }, + "name": "Demonic jibwings (e)", + "outputItems": Bank { + "bank": { + "73102": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 105, + "invention": 105, + "smithing": 105, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "48207": 1000, + "73098": 1, + }, + "frozen": false, + }, + "name": "Abyssal jibwings (e)", + "outputItems": Bank { + "bank": { + "73103": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 105, + "invention": 105, + "smithing": 105, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73090": 1000000, + }, + "frozen": false, + }, + "materialCost": MaterialBank { + "bank": { + "precious": 10000, + }, + }, + "name": "Divine ring", + "outputItems": Bank { + "bank": { + "73101": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 105, + "invention": 105, + "smithing": 105, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "50021": 1000, + "73090": 200000, + }, + "frozen": false, + }, + "materialCost": MaterialBank { + "bank": { + "orikalkum": 100, + }, + }, + "name": "Impling locator", + "outputItems": Bank { + "bank": { + "73100": 1, + }, + "frozen": false, + }, + "requiredSkills": { + "crafting": 105, + "invention": 105, + "magic": 120, + "smithing": 105, + }, + }, { "cantHaveItems": undefined, "inputItems": Bank { @@ -33793,5 +34258,75 @@ exports[`BSO Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "506": 1, + "52671": 1, + }, + "frozen": false, + }, + "name": "Axe handle base", + "outputItems": Bank { + "bank": { + "73093": 1, + }, + "frozen": false, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "47517": 1, + "50011": 10, + "50028": 30, + "73093": 1, + }, + "frozen": false, + }, + "name": "Axe handle", + "outputItems": Bank { + "bank": { + "73096": 1, + }, + "frozen": false, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73095": 1, + "73096": 1, + }, + "frozen": false, + }, + "name": "Axe of the high sungod (u)", + "outputItems": Bank { + "bank": { + "73074": 1, + }, + "frozen": false, + }, + }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "73074": 1, + "73090": 2000000, + }, + "frozen": false, + }, + "name": "Axe of the high sungod", + "outputItems": Bank { + "bank": { + "73072": 1, + }, + "frozen": false, + }, + }, ] `; diff --git a/tests/unit/snapshots/clsnapshots.test.ts.snap b/tests/unit/snapshots/clsnapshots.test.ts.snap index 42fc797ebb..fabeba2a88 100644 --- a/tests/unit/snapshots/clsnapshots.test.ts.snap +++ b/tests/unit/snapshots/clsnapshots.test.ts.snap @@ -42,7 +42,7 @@ Cooking (29) Corporeal Beast (8) Crafting (175) Crazy archaeologist (3) -Creatables (715) +Creatables (728) Creature Creation (7) Creature Creation (7) Custom Pets (42) @@ -147,13 +147,14 @@ Skilling Pets (8) Skotizo (6) Slayer (75) Slayer Masks/Helms (32) -Smithing (199) +Smithing (200) +Solis (3) Soul Wars (3) Spectator Clothes (22) Spooky crate (s3) (22) Stealing Creation (6) Supply crate (s1) (19) -Tame Gear (17) +Tame Gear (25) Temple Trekking (4) Tempoross (12) Thanksgiving 2021 (5) @@ -195,6 +196,8 @@ exports[`BSO Overall Collection Log Items 1`] = ` 3rd age druidic robe top 3rd age druidic staff 3rd age full helmet +3rd age jibwings +3rd age jibwings (e) 3rd age kiteshield 3rd age longsword 3rd age mage hat @@ -225,6 +228,8 @@ Abyssal dagger Abyssal gem Abyssal green dye Abyssal head +Abyssal jibwings +Abyssal jibwings (e) Abyssal lantern Abyssal mask Abyssal needle @@ -758,6 +763,8 @@ Decorative ranged legs Decorative ranged top Deerstalker Demon feet +Demonic jibwings +Demonic jibwings (e) Dexterous prayer scroll Dharok's greataxe Dharok's helm @@ -767,6 +774,7 @@ Digsite teleport Dinh's bulwark Divine egg Divine hand +Divine ring Divine sigil Diviner's footwear Diviner's handwear @@ -846,6 +854,7 @@ Dwarven knife Dwarven ore Dwarven pickaxe Dwarven toolkit +Eagle egg Earth warrior champion scroll Echo Ectoplasmator @@ -1190,6 +1199,7 @@ Imbued heart Imcando hammer Imp champion scroll Imp mask +Impling locator Incandescent energy Infernal cape Infernal core @@ -1852,6 +1862,7 @@ Smoke quartz Smolcano Smouldering stone Soaked page +Solite Sparkling energy Spectral sigil Spellbound ring @@ -1897,6 +1908,7 @@ Studded body (g) Studded body (t) Studded chaps (g) Studded chaps (t) +Sun-metal scraps Superior bonecrusher Superior dwarf multicannon Superior inferno adze