From fb2c4d2fc2f54db613bc99830f79395cfeb46015 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Sat, 22 Jun 2024 00:03:16 +1000 Subject: [PATCH] commit --- prisma/schema.prisma | 3 + src/lib/MUser.ts | 29 ++++ src/lib/colosseum.ts | 162 ++++++++++++++++-- .../combat_achievements/combatAchievements.ts | 3 + src/lib/combat_achievements/elite.ts | 37 +++- src/lib/combat_achievements/grandmaster.ts | 58 +++++++ src/lib/combat_achievements/master.ts | 59 +++++++ src/lib/data/Collections.ts | 18 ++ src/lib/data/creatablesTable.txt | 3 + src/lib/data/createables.ts | 16 ++ src/lib/degradeableItems.ts | 22 +++ src/lib/resources/images/minimus.png | Bin 0 -> 2664 bytes src/lib/settings/minigames.ts | 5 + src/lib/util/chatHeadImage.ts | 7 +- src/mahoji/commands/gamble.ts | 19 +- .../lib/abstracted_commands/capegamble.ts | 78 ++++++--- src/tasks/minions/colosseumActivity.ts | 10 +- tests/unit/snapshots/clsnapshots.test.ts.snap | 11 +- 18 files changed, 487 insertions(+), 53 deletions(-) create mode 100644 src/lib/resources/images/minimus.png diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3246ac3f35..c2f03ebe69 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -574,6 +574,7 @@ model Minigame { nmz Int @default(0) shades_of_morton Int @default(0) tombs_of_amascut Int @default(0) + colosseum Int @default(0) new_user NewUser? @@ -772,6 +773,8 @@ model UserStats { colo_kc_bank Json @default("{}") @db.Json colo_max_glory Int? + quivers_sacrificed Int @default(0) + @@map("user_stats") } diff --git a/src/lib/MUser.ts b/src/lib/MUser.ts index 0ece1e2048..85dc4bf603 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -1,3 +1,4 @@ +import { mentionCommand } from '@oldschoolgg/toolkit'; import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; import { GearSetupType, Prisma, User, UserStats, xp_gains_skill_enum } from '@prisma/client'; import { userMention } from 'discord.js'; @@ -15,6 +16,7 @@ import { badges, BitField, Emoji, projectiles, usernameCache } from './constants import { bossCLItems } from './data/Collections'; import { allPetIDs } from './data/CollectionsExport'; import { getSimilarItems } from './data/similarItems'; +import { degradeableItems } from './degradeableItems'; import { GearSetup, UserFullGearSetup } from './gear/types'; import { handleNewCLItems } from './handleNewCLItems'; import { marketPriceOfBank } from './marketPrices'; @@ -33,6 +35,7 @@ import { getFarmingInfoFromUser } from './skilling/functions/getFarmingInfo'; import Farming from './skilling/skills/farming'; import { SkillsEnum } from './skilling/types'; import { BankSortMethod } from './sorts'; +import { ChargeBank } from './structures/Bank'; import { defaultGear, Gear } from './structures/Gear'; import { ItemBank, Skills } from './types'; import { addItemToBank, convertXPtoLVL, itemNameFromID } from './util'; @@ -485,6 +488,32 @@ GROUP BY data->>'clueID';`); return blowpipe; } + hasCharges(chargeBank: ChargeBank) { + const failureReasons: string[] = []; + for (const [keyName, chargesToDegrade] of chargeBank.entries()) { + const degradeableItem = degradeableItems.find(i => i.settingsKey === keyName); + if (!degradeableItem) { + throw new Error(`Invalid degradeable item key: ${keyName}`); + } + const currentCharges = this.user[degradeableItem.settingsKey]; + const newCharges = currentCharges - chargesToDegrade; + if (newCharges < 0) { + failureReasons.push( + `You don't have enough ${degradeableItem.item.name} charges, you need ${chargesToDegrade}, but you have only ${currentCharges}.` + ); + } + } + if (failureReasons.length > 0) { + return { + hasCharges: false, + fullUserString: `${failureReasons.join(', ')} + +Charge your items using ${mentionCommand(globalClient, 'minion', 'charge')}.` + }; + } + return { hasCharges: true }; + } + percentOfBossCLFinished() { const percentBossCLFinished = calcWhatPercent( this.cl.items().filter(i => bossCLItems.includes(i[0].id)).length, diff --git a/src/lib/colosseum.ts b/src/lib/colosseum.ts index 6b4f957e38..5a5386efde 100644 --- a/src/lib/colosseum.ts +++ b/src/lib/colosseum.ts @@ -1,7 +1,10 @@ +import { mentionCommand } from '@oldschoolgg/toolkit'; +import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; import { calcPercentOfNum, calcWhatPercent, clamp, + increaseNumByPercent, objectEntries, objectValues, percentChance, @@ -12,9 +15,12 @@ import { import { Bank, LootTable } from 'oldschooljs'; import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; +import { QuestID } from '../mahoji/lib/abstracted_commands/questCommand'; import { userStatsBankUpdate } from '../mahoji/mahojiSettings'; +import { degradeChargeBank } from './degradeableItems'; import { GearSetupType } from './gear/types'; import { trackLoot } from './lootTrack'; +import { ChargeBank } from './structures/Bank'; import { GeneralBank, GeneralBankType } from './structures/GeneralBank'; import { ItemBank, Skills } from './types'; import { ColoTaskOptions } from './types/minions'; @@ -261,7 +267,7 @@ const waves: Wave[] = [ waveNumber: 12, enemies: ['Sol Heredit'], table: new LootTable() - .every("Dizana's quiver") + .every("Dizana's quiver (uncharged)") .tertiary(200, 'Smol heredit') .add('Onyx bolts', 100, 792) .add('Rune warhammer', 8, 792) @@ -314,6 +320,45 @@ export class ColosseumWaveBank extends GeneralBank { } } +function calculateTimeInMs(kc: number): number { + const points: { kc: number; timeInMinutes: number }[] = [ + { kc: 0, timeInMinutes: 50 }, + { kc: 1, timeInMinutes: 40 }, + { kc: 10, timeInMinutes: 30 }, + { kc: 100, timeInMinutes: 20 }, + { kc: 300, timeInMinutes: 16 } + ]; + + if (kc <= 0) return points[0].timeInMinutes * 60 * 1000; + if (kc >= 300) return points[4].timeInMinutes * 60 * 1000; + + if (kc <= 1) { + const timeAtKc0 = points[0].timeInMinutes; + const timeAtKc1 = points[1].timeInMinutes; + const slope = timeAtKc1 - timeAtKc0; + return (timeAtKc0 + slope * kc) * 60 * 1000; + } + + if (kc <= 10) { + const timeAtKc1 = points[1].timeInMinutes; + const timeAtKc10 = points[2].timeInMinutes; + const slope = (timeAtKc10 - timeAtKc1) / (10 - 1); + return (timeAtKc1 + slope * (kc - 1)) * 60 * 1000; + } + + if (kc <= 100) { + const timeAtKc10 = points[2].timeInMinutes; + const timeAtKc100 = points[3].timeInMinutes; + const slope = (timeAtKc100 - timeAtKc10) / (100 - 10); + return (timeAtKc10 + slope * (kc - 10)) * 60 * 1000; + } + + const timeAtKc100 = points[3].timeInMinutes; + const timeAtKc300 = points[4].timeInMinutes; + const slope = (timeAtKc300 - timeAtKc100) / (300 - 100); + return (timeAtKc100 + slope * (kc - 100)) * 60 * 1000; +} + interface ColosseumResult { diedAt: number | null; loot: Bank | null; @@ -324,7 +369,13 @@ interface ColosseumResult { realDuration: number; } -const startColosseumRun = (options: { kcBank: ColosseumWaveBank; chosenWaveToStop: number }): ColosseumResult => { +const startColosseumRun = (options: { + kcBank: ColosseumWaveBank; + chosenWaveToStop: number; + hasScythe: boolean; + hasTBow: boolean; + hasVenBow: boolean; +}): ColosseumResult => { const debugMessages: string[] = []; const bank = new Bank(); @@ -340,12 +391,25 @@ const startColosseumRun = (options: { kcBank: ColosseumWaveBank; chosenWaveToSto const addedWaveKCBank = new ColosseumWaveBank(); + let waveDuration = calculateTimeInMs(options.kcBank.amount(12)) / 12; + + if (!options.hasScythe) { + waveDuration = increaseNumByPercent(waveDuration, 10); + } + if (!options.hasTBow) { + waveDuration = increaseNumByPercent(waveDuration, 10); + } + if (!options.hasVenBow) { + waveDuration = increaseNumByPercent(waveDuration, 7); + } + let realDuration = 0; - const fakeDuration = 12 * (Time.Minute * 3.5); + + const fakeDuration = 12 * waveDuration; let maxGlory = 0; for (const wave of waves) { - realDuration += Time.Minute * 3.5; + realDuration += waveDuration; const kc = options.kcBank.amount(wave.waveNumber) ?? 0; const kcSkill = waveKCSkillBank.amount(wave.waveNumber) ?? 0; const wavePerformance = exponentialPercentScale((totalKCSkillPercent + kcSkill) / 2); @@ -403,7 +467,13 @@ function simulateColosseumRuns() { while (!done) { attempts++; const stopAt = waves[waves.length - 1].waveNumber; - const result = startColosseumRun({ kcBank, chosenWaveToStop: stopAt }); + const result = startColosseumRun({ + kcBank, + chosenWaveToStop: stopAt, + hasScythe: true, + hasTBow: true, + hasVenBow: true + }); totalDuration += result.realDuration; kcBank.add(result.addedWaveKCBank); if (result.diedAt === null) { @@ -445,6 +515,14 @@ export async function colosseumCommand(user: MUser, channelID: string) { return `${user.usernameOrMention} is busy`; } + if (!user.user.finished_quest_ids.includes(QuestID.ChildrenOfTheSun)) { + return `You need to complete the "Children of the Sun" quest before you can enter the Colosseum. Send your minion to do the quest using: ${mentionCommand( + globalClient, + 'activities', + 'quest' + )}.`; + } + const skillReqs: Skills = { attack: 90, strength: 90, @@ -467,8 +545,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { body: resolveItems(['Torva platebody', 'Bandos chestplate']), legs: resolveItems(['Torva platelegs', 'Bandos tassets']), feet: resolveItems(['Primordial boots']), - ring: resolveItems(['Ultor ring', 'Berserker ring (i)']), - '2h': resolveItems(['Scythe of vitur']) + ring: resolveItems(['Ultor ring', 'Berserker ring (i)']) }, range: { cape: resolveItems(["Dizana's quiver", "Ava's assembler"]), @@ -477,9 +554,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { body: resolveItems(['Masori body (f)', 'Masori body', 'Armadyl chestplate']), legs: resolveItems(['Masori chaps (f)', 'Masori chaps', 'Armadyl chainskirt']), feet: resolveItems(['Pegasian boots']), - ring: resolveItems(['Venator ring', 'Archers ring (i)']), - ammo: resolveItems(['Dragon arrow']), - '2h': resolveItems(['Twisted bow']) + ring: resolveItems(['Venator ring', 'Archers ring (i)']) } }; @@ -506,25 +581,66 @@ export async function colosseumCommand(user: MUser, channelID: string) { } if (!rangeWeapons.some(i => user.gear.range.hasEquipped(i))) { - return `You need one of these equipped in your melee setup to enter the Colosseum: ${rangeWeapons + return `You need one of these equipped in your range setup to enter the Colosseum: ${rangeWeapons .map(itemNameFromID) .join(', ')}.`; } + const messages: string[] = []; + + const hasScythe = user.gear.melee.hasEquipped('Scythe of vitur', true, true); + const hasTBow = user.gear.range.hasEquipped('Twisted bow', true, true); + function calculateVenCharges(duration: number) { + return Math.floor((duration / Time.Minute) * 3); + } + const hasVenBow = user.owns('Venator bow') && user.user.venator_bow_charges >= calculateVenCharges(Time.Hour); + + const res = startColosseumRun({ + kcBank: new ColosseumWaveBank((await user.fetchStats({ colo_kc_bank: true })).colo_kc_bank as ItemBank), + chosenWaveToStop: 12, + hasScythe, + hasTBow, + hasVenBow + }); + const minutes = res.realDuration / Time.Minute; + + const chargeBank = new ChargeBank(); const cost = new Bank() .add('Saradomin brew(4)', 6) .add('Super restore(4)', 8) .add('Super combat potion(4)') .add('Bastion potion(4)'); - if (!user.owns(cost)) { - return `You need ${cost} to attempt the Colosseum.`; + const scytheChargesPerHour = 2500; + const scytheChargesPerMinute = scytheChargesPerHour / 60; + const scytheCharges = Math.ceil(minutes * scytheChargesPerMinute); + if (hasScythe) { + messages.push('10% boost for Scythe'); + chargeBank.add('scythe_of_vitur_charges', scytheCharges); + } + if (hasTBow) { + messages.push('10% boost for TBow'); + const arrowsNeeded = minutes * 3; + cost.add('Dragon arrow', arrowsNeeded); + } + if (hasVenBow) { + messages.push('7% boost for Venator bow'); + chargeBank.add('venator_bow_charges', calculateVenCharges(res.realDuration)); } - const res = startColosseumRun({ - kcBank: new ColosseumWaveBank((await user.fetchStats({ colo_kc_bank: true })).colo_kc_bank as ItemBank), - chosenWaveToStop: 12 - }); + chargeBank.add('blood_fury_charges', scytheCharges); + + const realCost = new Bank(); + try { + const result = await user.specialRemoveItems(cost); + realCost.add(result.realCost); + } catch (err: any) { + if (err instanceof UserError) { + return err.message; + } + throw err; + } + messages.push(`Removed ${realCost}`); await updateBankSetting('colo_cost', cost); await userStatsBankUpdate(user.id, 'colo_cost', cost); @@ -542,6 +658,16 @@ export async function colosseumCommand(user: MUser, channelID: string) { }); await user.removeItemsFromBank(cost); + if (chargeBank.length() > 0) { + const hasChargesResult = user.hasCharges(chargeBank); + if (!hasChargesResult.hasCharges) { + return hasChargesResult.fullUserString!; + } + + const degradeResults = await degradeChargeBank(user, chargeBank); + messages.push(degradeResults.map(i => i.userMessage).join(', ')); + } + await addSubTaskToActivityTask({ userID: user.id, channelID, @@ -555,5 +681,5 @@ export async function colosseumCommand(user: MUser, channelID: string) { return `${user.minionName} is now attempting the Colosseum. They will finish in around ${formatDuration( res.fakeDuration - )}, unless they die early. Removed ${cost}.`; + )}, unless they die early. ${messages.join(', ')}`; } diff --git a/src/lib/combat_achievements/combatAchievements.ts b/src/lib/combat_achievements/combatAchievements.ts index 0565bd6028..4a6574c39c 100644 --- a/src/lib/combat_achievements/combatAchievements.ts +++ b/src/lib/combat_achievements/combatAchievements.ts @@ -164,6 +164,9 @@ export const combatAchievementTripEffect: TripFinishEffect['fn'] = async ({ data if (dataCopy.type === 'Inferno' && !dataCopy.diedPreZuk && !dataCopy.diedZuk) { (dataCopy as any).quantity = 1; } + if (dataCopy.type === 'Colosseum') { + (dataCopy as any).quantity = 1; + } if (!('quantity' in dataCopy)) return; let quantity = Number(dataCopy.quantity); if (isNaN(quantity)) return; diff --git a/src/lib/combat_achievements/elite.ts b/src/lib/combat_achievements/elite.ts index 3ce588a300..1d92077965 100644 --- a/src/lib/combat_achievements/elite.ts +++ b/src/lib/combat_achievements/elite.ts @@ -11,7 +11,7 @@ import { import { anyoneDiedInTOARaid } from '../simulation/toa'; import { SkillsEnum } from '../skilling/types'; import { Requirements } from '../structures/Requirements'; -import { GauntletOptions, NightmareActivityTaskOptions, TOAOptions } from '../types/minions'; +import { ActivityTaskData, GauntletOptions, NightmareActivityTaskOptions, TOAOptions } from '../types/minions'; import { isCertainMonsterTrip } from './caUtils'; import { type CombatAchievement } from './combatAchievements'; @@ -1491,5 +1491,40 @@ export const eliteCombatAchievements: CombatAchievement[] = [ [Monsters.Zulrah.id]: 75 } }) + }, + { + id: 1129, + name: 'I was here first!', + desc: 'Kill a Jaguar Warrior using a Claw-type weapon special attack.', + type: 'mechanical', + monster: 'Colosseum', + rng: { + chancePerKill: 5, + hasChance: 'Colosseum' + } + }, + { + id: 1130, + name: 'Denied', + desc: 'Complete Wave 7 without the Minotaur ever healing other enemies.', + type: 'mechanical', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => + data.type === 'Colosseum' && (!data.diedAt || (Boolean(data.diedAt) && data.diedAt < 7)) + } + }, + { + id: 1131, + name: 'Furball', + desc: 'Complete Wave 4 without taking avoidable damage from a Manticore.', + type: 'perfection', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => + data.type === 'Colosseum' && (!data.diedAt || (Boolean(data.diedAt) && data.diedAt < 4)) + } } ]; diff --git a/src/lib/combat_achievements/grandmaster.ts b/src/lib/combat_achievements/grandmaster.ts index 4e80b29d21..d6ca5dc3c9 100644 --- a/src/lib/combat_achievements/grandmaster.ts +++ b/src/lib/combat_achievements/grandmaster.ts @@ -1,9 +1,11 @@ +import { Time } from 'e'; import { Monsters } from 'oldschooljs'; import { PHOSANI_NIGHTMARE_ID } from '../constants'; import { anyoneDiedInTOARaid } from '../simulation/toa'; import { Requirements } from '../structures/Requirements'; import { + ActivityTaskData, GauntletOptions, NexTaskOptions, NightmareActivityTaskOptions, @@ -1026,5 +1028,61 @@ export const grandmasterCombatAchievements: CombatAchievement[] = [ chancePerKill: 110, hasChance: isCertainMonsterTrip(Monsters.Zulrah.id) } + }, + { + id: 3090, + name: 'Colosseum Speed-Runner', + desc: 'Complete the Colosseum with a total time of 24:00 or less.', + type: 'speed', + monster: 'Colosseum', + rng: { + chancePerKill: 1, + hasChance: (data: ActivityTaskData) => data.type === 'Colosseum' && data.duration < Time.Minute * 24 + } + }, + { + id: 3091, + name: 'Slow Dancing in the Sand', + desc: 'Defeat Sol Heredit without running during the fight with him.', + type: 'restriction', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => data.type === 'Colosseum' && !data.diedAt + } + }, + { + id: 3092, + name: 'Reinforcements', + desc: 'Defeat Sol Heredit with "Bees II", "Quartet" and "Solarflare II" modifiers active.', + type: 'mechanical', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => data.type === 'Colosseum' && !data.diedAt + } + }, + { + id: 3093, + name: 'Perfect Footwork', + desc: 'Defeat Sol Heredit without taking any damage from his Spear, Shield, Grapple or Triple Attack.', + type: 'perfection', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => data.type === 'Colosseum' && !data.diedAt + } + }, + { + id: 3094, + name: 'Colosseum Grand Champion', + desc: 'Defeat Sol Heredit 10 times.', + type: 'kill_count', + monster: 'Colosseum', + requirements: new Requirements().add({ + minigames: { + colosseum: 10 + } + }) } ]; diff --git a/src/lib/combat_achievements/master.ts b/src/lib/combat_achievements/master.ts index 1aaeca68da..868da5c6ca 100644 --- a/src/lib/combat_achievements/master.ts +++ b/src/lib/combat_achievements/master.ts @@ -1,9 +1,11 @@ +import { Time } from 'e'; import { Monsters } from 'oldschooljs'; import { NEX_ID, NIGHTMARE_ID, PHOSANI_NIGHTMARE_ID } from '../constants'; import { anyoneDiedInTOARaid } from '../simulation/toa'; import { Requirements } from '../structures/Requirements'; import { + ActivityTaskData, GauntletOptions, MonsterActivityTaskOptions, NightmareActivityTaskOptions, @@ -1460,5 +1462,62 @@ export const masterCombatAchievements: CombatAchievement[] = [ chancePerKill: 75, hasChance: isCertainMonsterTrip(Monsters.Zulrah.id) } + }, + { + id: 2129, + name: 'One-off', + desc: "Complete Wave 11 with either 'Red Flag', 'Dynamic Duo', or 'Doom II' active.", + type: 'mechanical', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => + data.type === 'Colosseum' && (!data.diedAt || (Boolean(data.diedAt) && data.diedAt < 11)) + } + }, + { + id: 2130, + name: 'Showboating', + desc: 'Defeat Sol Heredit after using Fortis Salute to the north, east, south and west of the arena while he is below 10% hitpoints.', + type: 'mechanical', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => data.type === 'Colosseum' && !data.diedAt + } + }, + { + id: 2131, + name: 'I Brought Mine Too', + desc: 'Defeat Sol Heredit using only a Spear, Hasta or Halberd.', + type: 'restriction', + monster: 'Colosseum', + rng: { + chancePerKill: 15, + hasChance: (data: ActivityTaskData) => data.type === 'Colosseum' && !data.diedAt + } + }, + { + id: 2132, + name: 'Sportsmanship', + desc: 'Defeat Sol Heredit once.', + type: 'kill_count', + monster: 'Colosseum', + requirements: new Requirements().add({ + minigames: { + colosseum: 1 + } + }) + }, + { + id: 2133, + name: 'Colosseum Speed-Chaser', + desc: 'Complete the Colosseum with a total time of 28:00 or less.', + type: 'speed', + monster: 'Colosseum', + rng: { + chancePerKill: 1, + hasChance: (data: ActivityTaskData) => data.type === 'Colosseum' && data.duration < Time.Minute * 28 + } } ]; diff --git a/src/lib/data/Collections.ts b/src/lib/data/Collections.ts index b72ed565e1..64353681b1 100644 --- a/src/lib/data/Collections.ts +++ b/src/lib/data/Collections.ts @@ -317,6 +317,24 @@ export const allCollectionLogs: ICollection = { items: fightCavesCL, fmtProg: kcProg(Monsters.TzTokJad) }, + 'Fortis Colosseum': { + kcActivity: { + Default: async (_, minigameScores) => + minigameScores.find(i => i.minigame.column === 'colosseum')!.score + }, + alias: ['colosseum'], + items: resolveItems([ + 'Smol heredit', + "Dizana's quiver (uncharged)", + 'Sunfire fanatic cuirass', + 'Sunfire fanatic chausses', + 'Sunfire fanatic helm', + 'Echo crystal', + 'Tonalztics of ralos (uncharged)', + 'Sunfire splinters' + ]), + fmtProg: ({ minigames }) => `${minigames.colosseum} KC` + }, 'The Gauntlet': { alias: ['gauntlet', 'crystalline hunllef', 'hunllef'], kcActivity: { diff --git a/src/lib/data/creatablesTable.txt b/src/lib/data/creatablesTable.txt index 508d346878..64bbc56712 100644 --- a/src/lib/data/creatablesTable.txt +++ b/src/lib/data/creatablesTable.txt @@ -95,6 +95,8 @@ | Bone shortbow | 1x Yew shortbow, 1x Scurrius' spine | 1x Bone shortbow | 0 | | Bone staff | 1,000x Chaos rune, 1x Battlestaff, 1x Scurrius' spine | 1x Bone staff | 0 | | Venator bow (uncharged) | 5x Venator shard | 1x Venator bow (uncharged) | 0 | +| Blessed dizana's quiver | 150,000x Sunfire splinters, 1x Dizana's quiver (uncharged) | 1x Blessed dizana's quiver | 0 | +| Dizana's max cape | 1x Max cape, 1x Max hood, 1x Blessed dizana's quiver | 1x Dizana's max cape, 1x Dizana's max hood | 0 | | Revert tanzanite fang | 1x Tanzanite fang | 20,000x Zulrah's scales | 0 | | Revert toxic blowpipe (empty) | 1x Toxic blowpipe (empty) | 20,000x Zulrah's scales | 0 | | Revert magic fang | 1x Magic fang | 20,000x Zulrah's scales | 0 | @@ -160,6 +162,7 @@ | Revert great blue heron | 1x Great blue heron | 1x Heron | 0 | | Revert greatish guardian | 1x Greatish guardian | 1x Rift guardian, 1x Guardian's eye | 0 | | Revert xeric's talisman (inert) | 1x Xeric's talisman (inert) | 100x Lizardman fang | 0 | +| Revert Dizana's quiver (uncharged) | 1x Dizana's quiver (uncharged) | 4,000x Sunfire splinters | 0 | | Crystal pickaxe | 120x Crystal shard, 1x Dragon pickaxe, 1x Crystal tool seed | 1x Crystal pickaxe | 0 | | Crystal harpoon | 120x Crystal shard, 1x Dragon harpoon, 1x Crystal tool seed | 1x Crystal harpoon | 0 | | Crystal axe | 120x Crystal shard, 1x Dragon axe, 1x Crystal tool seed | 1x Crystal axe | 0 | diff --git a/src/lib/data/createables.ts b/src/lib/data/createables.ts index 04376d0c8e..4557444d6d 100644 --- a/src/lib/data/createables.ts +++ b/src/lib/data/createables.ts @@ -1364,6 +1364,12 @@ const Reverteables: Createable[] = [ [itemID('Lizardman fang')]: 100 }, noCl: true + }, + { + name: "Revert Dizana's quiver (uncharged)", + inputItems: new Bank().add("Dizana's quiver (uncharged)"), + outputItems: new Bank().add('Sunfire splinters', 4000), + noCl: true } ]; @@ -2378,6 +2384,16 @@ const Createables: Createable[] = [ inputItems: new Bank().add('Venator shard', 5).freeze(), outputItems: new Bank().add('Venator bow (uncharged)').freeze() }, + { + name: "Blessed dizana's quiver", + inputItems: new Bank().add('Sunfire splinters', 150_000).add("Dizana's quiver (uncharged)").freeze(), + outputItems: new Bank().add("Blessed dizana's quiver").freeze() + }, + { + name: "Dizana's max cape", + inputItems: new Bank().add("Blessed dizana's quiver").add('Max cape').add('Max hood').freeze(), + outputItems: new Bank().add("Dizana's max cape").add("Dizana's max hood").freeze() + }, ...Reverteables, ...crystalTools, ...ornamentKits, diff --git a/src/lib/degradeableItems.ts b/src/lib/degradeableItems.ts index 9215086703..4969338770 100644 --- a/src/lib/degradeableItems.ts +++ b/src/lib/degradeableItems.ts @@ -5,6 +5,7 @@ import Monster from 'oldschooljs/dist/structures/Monster'; import { GearSetupType } from './gear/types'; import { KillableMonster } from './minions/types'; +import { ChargeBank } from './structures/Bank'; import { assert } from './util'; import getOSItem from './util/getOSItem'; import itemID from './util/itemID'; @@ -404,3 +405,24 @@ export async function checkDegradeableItemCharges({ item, user }: { item: Item; assert(typeof currentCharges === 'number'); return currentCharges; } + +export async function degradeChargeBank(user: MUser, chargeBank: ChargeBank) { + const hasChargesResult = user.hasCharges(chargeBank); + if (!hasChargesResult.hasCharges) { + throw new Error( + `Tried to degrade a charge bank (${chargeBank}) for ${ + user.logName + }, but they don't have the required charges: ${JSON.stringify(hasChargesResult)}` + ); + } + + const results = []; + + for (const [key, chargesToDegrade] of chargeBank.entries()) { + const { item } = degradeableItems.find(i => i.settingsKey === key)!; + const result = await degradeItem({ item, chargesToDegrade, user }); + results.push(result); + } + + return results; +} diff --git a/src/lib/resources/images/minimus.png b/src/lib/resources/images/minimus.png new file mode 100644 index 0000000000000000000000000000000000000000..c2782c2acc500db2a857a1c1e8052e91e959f2ea GIT binary patch literal 2664 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 W3jqYc=!*gX0000 = { @@ -35,7 +37,8 @@ const names: Record = { ketKeh: 'Tzhaar-Ket-Keh', gertrude: 'Gertrude', antiSanta: 'Anti-Santa', - bunny: 'Easter Bunny' + bunny: 'Easter Bunny', + minimus: 'Minimus' }; export async function newChatHeadImage({ content, head }: { content: string; head: keyof typeof chatHeads }) { diff --git a/src/mahoji/commands/gamble.ts b/src/mahoji/commands/gamble.ts index a09d352d3b..2dec4e4bca 100644 --- a/src/mahoji/commands/gamble.ts +++ b/src/mahoji/commands/gamble.ts @@ -26,17 +26,18 @@ export const gambleCommand: OSBMahojiCommand = { */ { type: ApplicationCommandOptionType.Subcommand, - name: 'cape', - description: 'Allows you to gamble fire/infernal capes for a chance at the pets.', + name: 'item', + description: 'Allows you to gamble fire/infernal capes/quivers for a chance at the pets.', options: [ { type: ApplicationCommandOptionType.String, - name: 'type', - description: 'The cape you wish to gamble.', + name: 'item', + description: 'The item you wish to gamble.', required: false, choices: [ { name: 'fire', value: 'fire' }, - { name: 'infernal', value: 'infernal' } + { name: 'infernal', value: 'infernal' }, + { name: 'quiver', value: 'quiver' } ] }, { @@ -175,7 +176,7 @@ export const gambleCommand: OSBMahojiCommand = { guildID, userID }: CommandRunOptions<{ - cape?: { type?: string; autoconfirm?: boolean }; + item?: { item?: string; autoconfirm?: boolean }; dice?: { amount?: string }; duel?: { user: MahojiUserOption; amount?: string }; lucky_pick?: { amount: string }; @@ -185,9 +186,9 @@ export const gambleCommand: OSBMahojiCommand = { }>) => { const user = await mUserFetch(userID); - if (options.cape) { - if (options.cape.type) { - return capeGambleCommand(user, options.cape.type, interaction, options.cape.autoconfirm); + if (options.item) { + if (options.item.item) { + return capeGambleCommand(user, options.item.item, interaction, options.item.autoconfirm); } return capeGambleStatsCommand(user); } diff --git a/src/mahoji/lib/abstracted_commands/capegamble.ts b/src/mahoji/lib/abstracted_commands/capegamble.ts index 5ba803aea5..62cada7739 100644 --- a/src/mahoji/lib/abstracted_commands/capegamble.ts +++ b/src/mahoji/lib/abstracted_commands/capegamble.ts @@ -18,15 +18,62 @@ export async function capeGambleStatsCommand(user: MUser) { **Infernal Cape's Gambled:** ${stats.infernal_cape_sacrifices}`; } +const itemGambles = [ + { + type: 'fire', + item: getOSItem('Fire cape'), + trackerKey: 'firecapes_sacrificed', + chatHead: 'mejJal', + chance: 200, + success: { + loot: getOSItem('Tzrek-Jad'), + message: 'You lucky. Better train him good else TzTok-Jad find you, JalYt.' + }, + failMessage: (newSacrificedCount: number) => + `You not lucky. Maybe next time, JalYt. This is the ${formatOrdinal( + newSacrificedCount + )} time you gamble cape.` + }, + { + type: 'infernal', + item: getOSItem('Infernal cape'), + trackerKey: 'infernal_cape_sacrifices', + chatHead: 'ketKeh', + chance: 100, + success: { + loot: getOSItem('Jal-nib-rek'), + message: 'Luck be a TzHaar tonight. Jal-Nib-Rek is yours.' + }, + failMessage: (newSacrificedCount: number) => + `No Jal-Nib-Rek for you. This is the ${formatOrdinal(newSacrificedCount)} time you gamble cape.` + }, + { + type: 'quiver', + item: getOSItem("Dizana's quiver (uncharged)"), + trackerKey: 'quivers_sacrificed', + chatHead: 'minimus', + chance: 200, + success: { + loot: getOSItem('Smol heredit'), + message: 'He seems to like you. Smol heredit is yours.' + }, + failMessage: (newSacrificedCount: number) => + `He doesn't want to go with you. Sorry. This is the ${formatOrdinal( + newSacrificedCount + )} time you gambled a quiver.` + } +] as const; + export async function capeGambleCommand( user: MUser, type: string, interaction: ChatInputCommandInteraction, autoconfirm: boolean = false ) { - const item = getOSItem(type === 'fire' ? 'Fire cape' : 'Infernal cape'); - const key: 'infernal_cape_sacrifices' | 'firecapes_sacrificed' = - type === 'fire' ? 'firecapes_sacrificed' : 'infernal_cape_sacrifices'; + const src = itemGambles.find(i => i.type === type); + if (!src) return 'Invalid type. You can only gamble fire capes, infernal capes, or quivers.'; + const { item } = src; + const key = src.trackerKey; const capesOwned = user.bank.amount(item.id); if (capesOwned < 1) return `You have no ${item.name}'s to gamble!`; @@ -48,13 +95,14 @@ export async function capeGambleCommand( }, { infernal_cape_sacrifices: true, - firecapes_sacrificed: true + firecapes_sacrificed: true, + quivers_sacrificed: true } ); const newSacrificedCount = newStats[key]; - const chance = type === 'fire' ? 200 : 100; - const pet = getOSItem(type === 'fire' ? 'Tzrek-Jad' : 'Jal-nib-rek'); + const { chance } = src; + const pet = src.success.loot; const gotPet = roll(chance); const loot = gotPet ? new Bank().add(pet.id) : undefined; @@ -72,11 +120,8 @@ export async function capeGambleCommand( { name: 'image.jpg', attachment: await newChatHeadImage({ - content: - type === 'fire' - ? 'You lucky. Better train him good else TzTok-Jad find you, JalYt.' - : 'Luck be a TzHaar tonight. Jal-Nib-Rek is yours.', - head: type === 'fire' ? 'mejJal' : 'ketKeh' + content: src.success.message, + head: src.chatHead }) } ] @@ -88,15 +133,8 @@ export async function capeGambleCommand( { name: 'image.jpg', attachment: await newChatHeadImage({ - content: - type === 'fire' - ? `You not lucky. Maybe next time, JalYt. This is the ${formatOrdinal( - newSacrificedCount - )} time you gamble cape.` - : `No Jal-Nib-Rek for you. This is the ${formatOrdinal( - newSacrificedCount - )} time you gamble cape.`, - head: type === 'fire' ? 'mejJal' : 'ketKeh' + content: src.failMessage(newSacrificedCount), + head: src.chatHead }) } ] diff --git a/src/tasks/minions/colosseumActivity.ts b/src/tasks/minions/colosseumActivity.ts index 7f18ff3654..01b08b61a7 100644 --- a/src/tasks/minions/colosseumActivity.ts +++ b/src/tasks/minions/colosseumActivity.ts @@ -3,8 +3,10 @@ import { ItemBank } from 'oldschooljs/dist/meta/types'; import { ColosseumWaveBank } from '../../lib/colosseum'; import { trackLoot } from '../../lib/lootTrack'; +import { incrementMinigameScore } from '../../lib/settings/minigames'; import { ColoTaskOptions } from '../../lib/types/minions'; import { handleTripFinish } from '../../lib/util/handleTripFinish'; +import { makeBankImage } from '../../lib/util/makeBankImage'; import { updateBankSetting } from '../../lib/util/updateBankSetting'; import { userStatsBankUpdate, userStatsUpdate } from '../../mahoji/mahojiSettings'; @@ -38,8 +40,10 @@ export const colosseumTask: MinionTask = { ); } + await incrementMinigameScore(user.id, 'colosseum'); + const loot = new Bank().add(possibleLoot); - await user.addItemsToBank({ items: loot, collectionLog: true }); + const { previousCL } = await user.addItemsToBank({ items: loot, collectionLog: true }); await updateBankSetting('colo_loot', loot); await userStatsBankUpdate(user.id, 'colo_loot', loot); @@ -66,6 +70,8 @@ export const colosseumTask: MinionTask = { str += ` Your new max glory is ${maxGlory}!`; } - return handleTripFinish(user, channelID, str, undefined, data, loot); + const image = await makeBankImage({ bank: loot, title: 'Colosseum Loot', user, previousCL }); + + return handleTripFinish(user, channelID, str, image.file.attachment, data, loot); } }; diff --git a/tests/unit/snapshots/clsnapshots.test.ts.snap b/tests/unit/snapshots/clsnapshots.test.ts.snap index 328e187c4b..e8c30a8a43 100644 --- a/tests/unit/snapshots/clsnapshots.test.ts.snap +++ b/tests/unit/snapshots/clsnapshots.test.ts.snap @@ -25,7 +25,7 @@ Chompy Birds (19) Commander Zilyana (8) Corporeal Beast (7) Crazy archaeologist (3) -Creatables (662) +Creatables (665) Creature Creation (7) Cyclopes (8) Dagannoth Kings (10) @@ -37,6 +37,7 @@ Elite Treasure Trails (59) Farming (141) Fishing Trawler (4) Forestry (23) +Fortis Colosseum (8) Fossil Island Notes (10) General Graardor (8) Giant Mole (3) @@ -582,6 +583,7 @@ Dharok's platebody Dharok's platelegs Digsite teleport Dinh's bulwark +Dizana's quiver (uncharged) Double ammo mould Draconic visage Dragon 2h sword @@ -625,6 +627,7 @@ Drake's tooth Dual sai Dust battlestaff Earth warrior champion scroll +Echo crystal Ectoplasmator Ecumenical key Elder chaos hood @@ -1360,6 +1363,7 @@ Smiths trousers Smiths tunic Smoke battlestaff Smoke quartz +Smol heredit Smolcano Smouldering stone Soaked page @@ -1400,6 +1404,10 @@ Studded body (t) Studded chaps (g) Studded chaps (t) Sturdy beehive parts +Sunfire fanatic chausses +Sunfire fanatic cuirass +Sunfire fanatic helm +Sunfire splinters Superior mining gloves Swift blade Tackle box @@ -1426,6 +1434,7 @@ Toktz-xil-ek Toktz-xil-ul Tome of fire Tome of water (empty) +Tonalztics of ralos (uncharged) Top hat Top of sceptre Torag's hammers