diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 50299c0658..3246ac3f35 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -134,6 +134,8 @@ model ClientStorage { nmz_cost Json @default("{}") @db.Json toa_cost Json @default("{}") @db.Json toa_loot Json @default("{}") @db.Json + colo_cost Json @default("{}") @db.Json + colo_loot Json @default("{}") @db.Json grand_exchange_is_locked Boolean @default(false) grand_exchange_total_tax BigInt @default(0) @@ -737,11 +739,11 @@ model UserStats { high_gambles Int @default(0) honour_points Int @default(0) - slayer_task_streak Int @default(0) - slayer_wildy_task_streak Int @default(0) - slayer_superior_count Int @default(0) - slayer_unsired_offered Int @default(0) - slayer_chewed_offered Int @default(0) + slayer_task_streak Int @default(0) + slayer_wildy_task_streak Int @default(0) + slayer_superior_count Int @default(0) + slayer_unsired_offered Int @default(0) + slayer_chewed_offered Int @default(0) tob_cost Json @default("{}") tob_loot Json @default("{}") @@ -765,6 +767,11 @@ model UserStats { forestry_event_completions_bank Json @default("{}") @db.Json + colo_cost Json @default("{}") @db.Json + colo_loot Json @default("{}") @db.Json + colo_kc_bank Json @default("{}") @db.Json + colo_max_glory Int? + @@map("user_stats") } @@ -945,6 +952,7 @@ enum activity_type_enum { CamdozaalFishing CamdozaalMining CamdozaalSmithing + Colosseum } enum xp_gains_skill_enum { diff --git a/src/lib/Task.ts b/src/lib/Task.ts index 2436441b74..11f2692f2e 100644 --- a/src/lib/Task.ts +++ b/src/lib/Task.ts @@ -11,6 +11,7 @@ import { camdozaalSmithingTask } from '../tasks/minions/camdozaalActivity/camdoz import { castingTask } from '../tasks/minions/castingActivity'; import { clueTask } from '../tasks/minions/clueActivity'; import { collectingTask } from '../tasks/minions/collectingActivity'; +import { colosseumTask } from '../tasks/minions/colosseumActivity'; import { combatRingTask } from '../tasks/minions/combatRingActivity'; import { constructionTask } from '../tasks/minions/constructionActivity'; import { cookingTask } from '../tasks/minions/cookingActivity'; @@ -183,7 +184,8 @@ export const tasks: MinionTask[] = [ specificQuestTask, camdozaalMiningTask, camdozaalSmithingTask, - camdozaalFishingTask + camdozaalFishingTask, + colosseumTask ]; export async function processPendingActivities() { diff --git a/src/lib/colosseum.ts b/src/lib/colosseum.ts index 1eaeb79be4..6b4f957e38 100644 --- a/src/lib/colosseum.ts +++ b/src/lib/colosseum.ts @@ -1,9 +1,34 @@ -import { calcPercentOfNum, calcWhatPercent, clamp, percentChance, randFloat, randInt, sumArr, Time } from 'e'; +import { + calcPercentOfNum, + calcWhatPercent, + clamp, + objectEntries, + objectValues, + percentChance, + randInt, + sumArr, + Time +} from 'e'; import { Bank, LootTable } from 'oldschooljs'; +import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; +import { userStatsBankUpdate } from '../mahoji/mahojiSettings'; +import { GearSetupType } from './gear/types'; +import { trackLoot } from './lootTrack'; import { GeneralBank, GeneralBankType } from './structures/GeneralBank'; +import { ItemBank, Skills } from './types'; +import { ColoTaskOptions } from './types/minions'; import { assert } from './util'; -import { averageBank, exponentialPercentScale, formatDuration } from './util/smallUtils'; +import addSubTaskToActivityTask from './util/addSubTaskToActivityTask'; +import resolveItems from './util/resolveItems'; +import { + averageBank, + exponentialPercentScale, + formatDuration, + formatSkillRequirements, + itemNameFromID +} from './util/smallUtils'; +import { updateBankSetting } from './util/updateBankSetting'; interface Wave { waveNumber: number; @@ -12,12 +37,6 @@ interface Wave { table: LootTable; } -interface Handicap { - name: string; - effect: string; - tier?: number; -} - const waves: Wave[] = [ { waveNumber: 1, @@ -271,11 +290,16 @@ const calculateLootForWave = (wave: Wave) => { function calculateDeathChance(kc: number, waveNumber: number): number { const cappedKc = Math.min(Math.max(kc, 0), 1000); - const baseChance = 75; + const baseChance = 65; const kcReduction = Math.min(90, 90 * (1 - Math.exp(-0.1 * cappedKc))); // Steep reduction for the first few kc const waveIncrease = waveNumber * 1.5; - const deathChance = Math.max(1, Math.min(99, baseChance - kcReduction + waveIncrease)); + let newChance = baseChance - kcReduction + waveIncrease; + if (kc > 0) { + newChance /= 10; + } + + const deathChance = Math.max(1, Math.min(80, newChance)); return deathChance; } @@ -296,6 +320,8 @@ interface ColosseumResult { maxGlory: number; addedWaveKCBank: ColosseumWaveBank; debugMessages: string[]; + fakeDuration: number; + realDuration: number; } const startColosseumRun = (options: { kcBank: ColosseumWaveBank; chosenWaveToStop: number }): ColosseumResult => { @@ -314,15 +340,19 @@ const startColosseumRun = (options: { kcBank: ColosseumWaveBank; chosenWaveToSto const addedWaveKCBank = new ColosseumWaveBank(); + let realDuration = 0; + const fakeDuration = 12 * (Time.Minute * 3.5); + let maxGlory = 0; for (const wave of waves) { + realDuration += Time.Minute * 3.5; const kc = options.kcBank.amount(wave.waveNumber) ?? 0; const kcSkill = waveKCSkillBank.amount(wave.waveNumber) ?? 0; const wavePerformance = exponentialPercentScale((totalKCSkillPercent + kcSkill) / 2); const glory = randInt(calcPercentOfNum(wavePerformance, ourMaxGlory), ourMaxGlory); maxGlory = Math.max(glory, maxGlory); - const deathChance = calculateDeathChance(kc, wave.waveNumber); - // console.log({ expSkill, wavePerformance, totalKCSkillPercent }); + let deathChance = calculateDeathChance(kc, wave.waveNumber); + // deathChance = reduceNumByPercent(deathChance, clamp(kc * 3, 1, 50)); debugMessages.push(`Wave ${wave.waveNumber} at ${kc}KC death chance: ${deathChance}%`); if (percentChance(deathChance)) { @@ -331,7 +361,9 @@ const startColosseumRun = (options: { kcBank: ColosseumWaveBank; chosenWaveToSto loot: null, maxGlory: 0, addedWaveKCBank, - debugMessages + debugMessages, + fakeDuration, + realDuration }; } addedWaveKCBank.add(wave.waveNumber); @@ -343,7 +375,9 @@ const startColosseumRun = (options: { kcBank: ColosseumWaveBank; chosenWaveToSto loot: bank, maxGlory, addedWaveKCBank, - debugMessages + debugMessages, + fakeDuration, + realDuration }; } } @@ -352,11 +386,12 @@ const startColosseumRun = (options: { kcBank: ColosseumWaveBank; chosenWaveToSto }; function simulateColosseumRuns() { - const totalSimulations = 200; + const totalSimulations = 500; let totalAttempts = 0; let totalDeaths = 0; const totalLoot = new Bank(); const finishAttemptAmounts = []; + let totalDuration = 0; for (let i = 0; i < totalSimulations; i++) { let attempts = 0; @@ -369,8 +404,8 @@ function simulateColosseumRuns() { attempts++; const stopAt = waves[waves.length - 1].waveNumber; const result = startColosseumRun({ kcBank, chosenWaveToStop: stopAt }); + totalDuration += result.realDuration; kcBank.add(result.addedWaveKCBank); - // console.log(result.debugMessages.join(', ')); if (result.diedAt === null) { if (result.loot) runLoot.add(result.loot); done = true; @@ -384,7 +419,6 @@ function simulateColosseumRuns() { totalAttempts += attempts; totalDeaths += deaths; totalLoot.add(runLoot); - console.log(runLoot.toString()); } const averageAttempts = totalAttempts / totalSimulations; @@ -392,6 +426,7 @@ function simulateColosseumRuns() { finishAttemptAmounts.sort((a, b) => a - b); + console.log(`Avg duration: ${formatDuration(totalDuration / totalSimulations)}`); console.log(`Total simulations: ${totalSimulations}`); console.log( `Average attempts to beat wave 12: ${averageAttempts}. ${formatDuration( @@ -404,3 +439,121 @@ function simulateColosseumRuns() { } simulateColosseumRuns(); + +export async function colosseumCommand(user: MUser, channelID: string) { + if (user.minionIsBusy) { + return `${user.usernameOrMention} is busy`; + } + + const skillReqs: Skills = { + attack: 90, + strength: 90, + defence: 90, + prayer: 80, + ranged: 90, + magic: 94, + hitpoints: 90 + }; + + if (!user.hasSkillReqs(skillReqs)) { + return `You need ${formatSkillRequirements(skillReqs)} to enter the Colosseum.`; + } + + const requiredItems: Partial>>> = { + melee: { + head: resolveItems(['Torva full helm', 'Neitiznot faceguard', 'Justiciar faceguard']), + cape: resolveItems(['Infernal cape', 'Fire cape']), + neck: resolveItems(['Amulet of blood fury']), + 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']) + }, + range: { + cape: resolveItems(["Dizana's quiver", "Ava's assembler"]), + head: resolveItems(['Masori mask (f)', 'Masori mask', 'Armadyl helmet']), + neck: resolveItems(['Necklace of anguish']), + 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']) + } + }; + + const meleeWeapons = resolveItems(['Scythe of vitur', 'Blade of saeldor (c)']); + const rangeWeapons = resolveItems(['Twisted bow', 'Bow of faerdhinen (c)']); + + for (const [gearType, gearNeeded] of objectEntries(requiredItems)) { + const gear = user.gear[gearType]; + if (!gearNeeded) continue; + for (const items of objectValues(gearNeeded)) { + if (!items) continue; + if (!items.some(g => gear.hasEquipped(g))) { + return `You need one of these equipped in your ${gearType} setup to enter the Colosseum: ${items + .map(itemNameFromID) + .join(', ')}.`; + } + } + } + + if (!meleeWeapons.some(i => user.gear.melee.hasEquipped(i))) { + return `You need one of these equipped in your melee setup to enter the Colosseum: ${meleeWeapons + .map(itemNameFromID) + .join(', ')}.`; + } + + 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 + .map(itemNameFromID) + .join(', ')}.`; + } + + 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 res = startColosseumRun({ + kcBank: new ColosseumWaveBank((await user.fetchStats({ colo_kc_bank: true })).colo_kc_bank as ItemBank), + chosenWaveToStop: 12 + }); + + await updateBankSetting('colo_cost', cost); + await userStatsBankUpdate(user.id, 'colo_cost', cost); + await trackLoot({ + totalCost: cost, + id: 'colo', + type: 'Minigame', + changeType: 'cost', + users: [ + { + id: user.id, + cost + } + ] + }); + await user.removeItemsFromBank(cost); + + await addSubTaskToActivityTask({ + userID: user.id, + channelID, + duration: res.realDuration, + type: 'Colosseum', + fakeDuration: res.fakeDuration, + maxGlory: res.maxGlory, + diedAt: res.diedAt ?? undefined, + loot: res.loot?.bank + }); + + return `${user.minionName} is now attempting the Colosseum. They will finish in around ${formatDuration( + res.fakeDuration + )}, unless they die early. Removed ${cost}.`; +} diff --git a/src/lib/structures/GeneralBank.ts b/src/lib/structures/GeneralBank.ts index eed978130a..6fe5fa5678 100644 --- a/src/lib/structures/GeneralBank.ts +++ b/src/lib/structures/GeneralBank.ts @@ -41,6 +41,10 @@ export class GeneralBank { this.validate(); } + get _bank() { + return this.bank; + } + clone(): GeneralBank { return new GeneralBank({ allowedKeys: this.allowedKeys ? Array.from(this.allowedKeys) : undefined, diff --git a/src/lib/types/minions.ts b/src/lib/types/minions.ts index 42b6b8512f..a28ba90dcd 100644 --- a/src/lib/types/minions.ts +++ b/src/lib/types/minions.ts @@ -449,6 +449,14 @@ export interface TheatreOfBloodTaskOptions extends ActivityTaskOptionsWithUsers solo?: boolean; } +export interface ColoTaskOptions extends ActivityTaskOptions { + type: 'Colosseum'; + fakeDuration: number; + diedAt?: number; + loot?: ItemBank; + maxGlory: number; +} + type UserID = string; type Points = number; type RoomIDsDiedAt = number[]; @@ -600,4 +608,5 @@ export type ActivityTaskData = | FightCavesActivityTaskOptions | ActivityTaskOptionsWithQuantity | MinigameActivityTaskOptionsWithNoChanges - | CutLeapingFishActivityTaskOptions; + | CutLeapingFishActivityTaskOptions + | ColoTaskOptions; diff --git a/src/lib/util/minionStatus.ts b/src/lib/util/minionStatus.ts index 4dc7f1bb8c..51960fbf33 100644 --- a/src/lib/util/minionStatus.ts +++ b/src/lib/util/minionStatus.ts @@ -36,6 +36,7 @@ import { CastingActivityTaskOptions, ClueActivityTaskOptions, CollectingOptions, + ColoTaskOptions, ConstructionActivityTaskOptions, CookingActivityTaskOptions, CraftingActivityTaskOptions, @@ -681,6 +682,14 @@ export function minionStatus(user: MUser) { quests.find(i => i.id === data.questID)!.name }! The trip should take ${formatDuration(durationRemaining)}.`; } + case 'Colosseum': { + const data = currentTask as ColoTaskOptions; + const durationRemaining = data.finishDate - data.duration + data.fakeDuration - Date.now(); + + return `${name} is currently attempting the Colosseum, if they are successful, the trip should take ${formatDuration( + durationRemaining + )}.`; + } case 'HalloweenEvent': { return `${name} is doing the Halloween event! The trip should take ${formatDuration(durationRemaining)}.`; } diff --git a/src/lib/util/repeatStoredTrip.ts b/src/lib/util/repeatStoredTrip.ts index 70b24847d5..70074f213a 100644 --- a/src/lib/util/repeatStoredTrip.ts +++ b/src/lib/util/repeatStoredTrip.ts @@ -622,6 +622,12 @@ export const tripHandlers = { drift_net_fishing: { minutes: Math.floor(data.duration / Time.Minute) } } }) + }, + [activity_type_enum.Colosseum]: { + commandName: 'k', + args: () => ({ + name: 'colosseum' + }) } } as const; diff --git a/src/lib/util/updateBankSetting.ts b/src/lib/util/updateBankSetting.ts index 201171fd2c..c95482c4ce 100644 --- a/src/lib/util/updateBankSetting.ts +++ b/src/lib/util/updateBankSetting.ts @@ -59,7 +59,9 @@ type ClientBankKey = | 'nex_loot' | 'nmz_cost' | 'toa_cost' - | 'toa_loot'; + | 'toa_loot' + | 'colo_cost' + | 'colo_loot'; export async function updateBankSetting(key: ClientBankKey, bankToAdd: Bank) { if (bankToAdd === undefined || bankToAdd === null) throw new Error(`Gave null bank for ${key}`); diff --git a/src/lib/util/userQueues.ts b/src/lib/util/userQueues.ts index 89edd77239..d3b818a04f 100644 --- a/src/lib/util/userQueues.ts +++ b/src/lib/util/userQueues.ts @@ -18,6 +18,7 @@ export async function userQueueFn(userID: string, fn: () => Promise) { try { return await fn(); } catch (e) { + console.error(e); error.message = (e as Error).message; throw error; } diff --git a/src/mahoji/commands/k.ts b/src/mahoji/commands/k.ts index ba1a225458..163be1a7f3 100644 --- a/src/mahoji/commands/k.ts +++ b/src/mahoji/commands/k.ts @@ -35,6 +35,11 @@ export const autocompleteMonsters = [ aliases: ['wt', 'wintertodt', 'todt'], id: -1, emoji: '<:Phoenix:324127378223792129>' + }, + { + name: 'Colosseum', + aliases: ['colo', 'colosseum'], + id: -1 } ]; diff --git a/src/mahoji/commands/testpotato.ts b/src/mahoji/commands/testpotato.ts index 1be86fafb3..f100cf19a0 100644 --- a/src/mahoji/commands/testpotato.ts +++ b/src/mahoji/commands/testpotato.ts @@ -27,6 +27,7 @@ import { slayerMasterChoices } from '../../lib/slayer/constants'; import { slayerMasters } from '../../lib/slayer/slayerMasters'; import { getUsersCurrentSlayerInfo } from '../../lib/slayer/slayerUtil'; import { allSlayerMonsters } from '../../lib/slayer/tasks'; +import { Gear } from '../../lib/structures/Gear'; import { stringMatches } from '../../lib/util'; import { calcDropRatesFromBankWithoutUniques } from '../../lib/util/calcDropRatesFromBank'; import { @@ -38,6 +39,7 @@ import { import getOSItem from '../../lib/util/getOSItem'; import { logError } from '../../lib/util/logError'; import { parseStringBank } from '../../lib/util/parseStringBank'; +import resolveItems from '../../lib/util/resolveItems'; import { userEventToStr } from '../../lib/util/userEvents'; import { getPOH } from '../lib/abstracted_commands/pohCommand'; import { MAX_QP } from '../lib/abstracted_commands/questCommand'; @@ -80,12 +82,47 @@ async function givePatronLevel(user: MUser, tier: number) { return `Gave you tier ${tierToGive[1] - 1} patron.`; } +const coloMelee = new Gear(); +for (const gear of resolveItems([ + 'Torva full helm', + 'Infernal cape', + 'Amulet of blood fury', + 'Torva platebody', + 'Torva platelegs', + 'Primordial boots', + 'Ultor ring', + 'Scythe of vitur' +])) { + coloMelee.equip(getOSItem(gear)); +} + +const coloRange = new Gear(); +for (const gear of resolveItems([ + "Dizana's quiver", + 'Masori mask (f)', + 'Necklace of anguish', + 'Masori body (f)', + 'Masori chaps (f)', + 'Pegasian boots', + 'Venator ring', + 'Dragon arrow', + 'Twisted bow' +])) { + coloRange.equip(getOSItem(gear)); +} + const gearPresets = [ { name: 'ToB', melee: TOBMaxMeleeGear, mage: TOBMaxMageGear, range: TOBMaxRangeGear + }, + { + name: 'Colosseum', + melee: coloMelee, + range: coloRange, + mage: coloRange } ]; diff --git a/src/mahoji/lib/abstracted_commands/minionKill.ts b/src/mahoji/lib/abstracted_commands/minionKill.ts index bd7b34d5fd..a140b3da8b 100644 --- a/src/mahoji/lib/abstracted_commands/minionKill.ts +++ b/src/mahoji/lib/abstracted_commands/minionKill.ts @@ -15,6 +15,7 @@ import { Bank, Monsters } from 'oldschooljs'; import { MonsterAttribute } from 'oldschooljs/dist/meta/monsterData'; import { itemID } from 'oldschooljs/dist/util'; +import { colosseumCommand } from '../../../lib/colosseum'; import { BitField, PeakTier, PvMMethod } from '../../../lib/constants'; import { Eatables } from '../../../lib/data/eatables'; import { getSimilarItems } from '../../../lib/data/similarItems'; @@ -156,6 +157,7 @@ export async function minionKillCommand( if (!name) return invalidMonsterMsg; + if (stringMatches(name, 'colosseum')) return colosseumCommand(user, channelID); if (stringMatches(name, 'nex')) return nexCommand(interaction, user, channelID, solo); if (stringMatches(name, 'zalcano')) return zalcanoCommand(user, channelID); if (stringMatches(name, 'tempoross')) return temporossCommand(user, channelID, quantity); diff --git a/src/tasks/minions/colosseumActivity.ts b/src/tasks/minions/colosseumActivity.ts new file mode 100644 index 0000000000..7f18ff3654 --- /dev/null +++ b/src/tasks/minions/colosseumActivity.ts @@ -0,0 +1,71 @@ +import { Bank } from 'oldschooljs'; +import { ItemBank } from 'oldschooljs/dist/meta/types'; + +import { ColosseumWaveBank } from '../../lib/colosseum'; +import { trackLoot } from '../../lib/lootTrack'; +import { ColoTaskOptions } from '../../lib/types/minions'; +import { handleTripFinish } from '../../lib/util/handleTripFinish'; +import { updateBankSetting } from '../../lib/util/updateBankSetting'; +import { userStatsBankUpdate, userStatsUpdate } from '../../mahoji/mahojiSettings'; + +export const colosseumTask: MinionTask = { + type: 'Colosseum', + async run(data: ColoTaskOptions) { + const { channelID, userID, loot: possibleLoot, diedAt, maxGlory } = data; + const user = await mUserFetch(userID); + + const newKCs = new ColosseumWaveBank(); + for (let i = 0; i < (diedAt ? diedAt - 1 : 12); i++) { + newKCs.add(i + 1); + } + const stats = await user.fetchStats({ colo_kc_bank: true, colo_max_glory: true }); + for (const [key, value] of Object.entries(stats.colo_kc_bank as ItemBank)) newKCs.add(parseInt(key), value); + await userStatsUpdate(user.id, { colo_kc_bank: newKCs._bank }); + const newKCsStr = `${newKCs + .entries() + .map(([kc, amount]) => `Wave ${kc}: ${amount} KC`) + .join(', ')}`; + + const newWaveKcStr = !diedAt || diedAt > 1 ? `New wave KCs: ${newKCsStr}.` : 'No new KCs.'; + if (diedAt) { + return handleTripFinish( + user, + channelID, + `${user}, you died on wave ${diedAt}, and received no loot. ${newWaveKcStr}`, + undefined, + data, + null + ); + } + + const loot = new Bank().add(possibleLoot); + await user.addItemsToBank({ items: loot, collectionLog: true }); + + await updateBankSetting('colo_loot', loot); + await userStatsBankUpdate(user.id, 'colo_loot', loot); + await trackLoot({ + totalLoot: loot, + id: 'colo', + type: 'Minigame', + changeType: 'loot', + duration: data.duration, + kc: 1, + users: [ + { + id: user.id, + loot, + duration: data.duration + } + ] + }); + + let str = `${user}, you completed the Colosseum! You received: ${loot}. ${newWaveKcStr}`; + + if (!stats.colo_max_glory || maxGlory > stats.colo_max_glory) { + await userStatsUpdate(user.id, { colo_max_glory: maxGlory }); + str += ` Your new max glory is ${maxGlory}!`; + } + + return handleTripFinish(user, channelID, str, undefined, data, loot); + } +};