From 4b1365164dbd6a7bd4c8568ce68eec8c2109efa3 Mon Sep 17 00:00:00 2001 From: themrrobert <10122432+themrrobert@users.noreply.github.com> Date: Thu, 1 Feb 2024 01:06:01 -0800 Subject: [PATCH 01/28] Fix tradePlayerItems / stale bank overwrite bug. (#5621) --- src/lib/util/tradePlayerItems.ts | 135 +++++++++++----------- tests/integration/paymentConflict.test.ts | 134 +++++++++++++++++++++ tests/integration/setup.ts | 3 +- 3 files changed, 202 insertions(+), 70 deletions(-) create mode 100644 tests/integration/paymentConflict.test.ts diff --git a/src/lib/util/tradePlayerItems.ts b/src/lib/util/tradePlayerItems.ts index 68404332d5..5b13639ef4 100644 --- a/src/lib/util/tradePlayerItems.ts +++ b/src/lib/util/tradePlayerItems.ts @@ -16,79 +16,76 @@ export async function tradePlayerItems(sender: MUser, recipient: MUser, _itemsTo const itemsToReceive = _itemsToReceive ? _itemsToReceive.clone() : new Bank(); // Queue the primary trade function: - const processTradePromise = userQueueFn(sender.id, async () => { - try { - await Promise.all([sender.sync(), recipient.sync()]); - if (!sender.owns(itemsToSend)) { - return { success: false, message: `${sender.usernameOrMention} doesn't own all items.` }; - } - if (!recipient.owns(itemsToReceive)) { - return { success: false, message: `${recipient.usernameOrMention} doesn't own all items.` }; - } - const newSenderBank = sender.bank.clone(); - const newRecipientBank = recipient.bank.clone(); - - let senderGP = sender.GP; - let recipientGP = recipient.GP; - - if (itemsToSend.has('Coins')) { - const sentCoins = itemsToSend.amount('Coins'); - senderGP -= sentCoins; - itemsToSend.remove('Coins', sentCoins); - recipientGP += sentCoins; - } - if (itemsToReceive.has('Coins')) { - const receivedCoins = itemsToReceive.amount('Coins'); - recipientGP -= receivedCoins; - itemsToReceive.remove('Coins', receivedCoins); - senderGP += receivedCoins; - } + return userQueueFn(sender.id, async () => { + return userQueueFn(recipient.id, async () => { + try { + await Promise.all([sender.sync(), recipient.sync()]); + if (!sender.owns(itemsToSend)) { + return { success: false, message: `${sender.usernameOrMention} doesn't own all items.` }; + } + if (!recipient.owns(itemsToReceive)) { + return { success: false, message: `${recipient.usernameOrMention} doesn't own all items.` }; + } + const newSenderBank = sender.bank.clone(); + const newRecipientBank = recipient.bank.clone(); - newSenderBank.remove(itemsToSend); - newRecipientBank.remove(itemsToReceive); - newSenderBank.add(itemsToReceive); - newRecipientBank.add(itemsToSend); + let senderGP = sender.GP; + let recipientGP = recipient.GP; - const updateSender = prisma.user.update({ - data: { - GP: senderGP, - bank: newSenderBank.bank - }, - where: { - id: sender.id + if (itemsToSend.has('Coins')) { + const sentCoins = itemsToSend.amount('Coins'); + senderGP -= sentCoins; + itemsToSend.remove('Coins', sentCoins); + recipientGP += sentCoins; } - }); - const updateRecipient = prisma.user.update({ - data: { - GP: recipientGP, - bank: newRecipientBank.bank - }, - where: { - id: recipient.id + if (itemsToReceive.has('Coins')) { + const receivedCoins = itemsToReceive.amount('Coins'); + recipientGP -= receivedCoins; + itemsToReceive.remove('Coins', receivedCoins); + senderGP += receivedCoins; } - }); - // Run both in a single transaction, so it all succeeds or all fails: - await prisma.$transaction([updateSender, updateRecipient]); - await Promise.all([sender.sync(), recipient.sync()]); - return { success: true, message: null }; - } catch (e: any) { - // We should only get here if something bad happened... most likely prisma, but possibly command competition - logError(e, undefined, { - sender: sender.id, - recipient: recipient.id, - command: 'trade', - items_sent: itemsToSend.toString(), - items_received: itemsToReceive.toString() - }); - return { success: false, message: 'Temporary error, please try again.' }; - } finally { - modifyBusyCounter(sender.id, -1); - modifyBusyCounter(recipient.id, -1); - } - }); - // Queue function for the recipient so no funny business / mistakes happen: - return userQueueFn(recipient.id, async () => { - return processTradePromise; + newSenderBank.remove(itemsToSend); + newRecipientBank.remove(itemsToReceive); + newSenderBank.add(itemsToReceive); + newRecipientBank.add(itemsToSend); + + const updateSender = prisma.user.update({ + data: { + GP: senderGP, + bank: newSenderBank.bank + }, + where: { + id: sender.id + } + }); + const updateRecipient = prisma.user.update({ + data: { + GP: recipientGP, + bank: newRecipientBank.bank + }, + where: { + id: recipient.id + } + }); + // Run both in a single transaction, so it all succeeds or all fails: + await prisma.$transaction([updateSender, updateRecipient]); + await Promise.all([sender.sync(), recipient.sync()]); + return { success: true, message: null }; + } catch (e: any) { + // We should only get here if something bad happened... most likely prisma, but possibly command competition + logError(e, undefined, { + sender: sender.id, + recipient: recipient.id, + command: 'trade', + items_sent: itemsToSend.toString(), + items_received: itemsToReceive.toString() + }); + return { success: false, message: 'Temporary error, please try again.' }; + } finally { + modifyBusyCounter(sender.id, -1); + modifyBusyCounter(recipient.id, -1); + } + }); }); } diff --git a/tests/integration/paymentConflict.test.ts b/tests/integration/paymentConflict.test.ts new file mode 100644 index 0000000000..fb0c1c0ba3 --- /dev/null +++ b/tests/integration/paymentConflict.test.ts @@ -0,0 +1,134 @@ +import { randomSnowflake } from '@oldschoolgg/toolkit'; +import { randArrItem, randInt, roll, Time } from 'e'; +import { Bank } from 'oldschooljs'; +import { describe, expect, test } from 'vitest'; + +import { payCommand } from '../../src/mahoji/commands/pay'; +import { createTestUser, mockClient, TestUser } from './util'; + +describe('Payment conflicts', async () => { + const payerCount = 50; + const iterations = 100; + const addChance = 3; + const repeats = 5; + + const bigBank = new Bank().add('Cannonball', 4).add('Bones', 10_000); + + test( + 'GE Simulation (Payee)', + async () => { + await mockClient(); + + // Payee is currently the primary target of the test. + const userPayee = await createTestUser(randomSnowflake(), new Bank(bigBank), { GP: 1_000_000_000 }); + + const payeeTarget = await globalClient.fetchUser(userPayee.id); + + const startingBallCount = userPayee.bank.amount('Cannonball'); + + const payers: TestUser[] = []; + for (let i = 0; i < payerCount; i++) { + payers.push(await createTestUser(randomSnowflake(), new Bank(), { GP: 1_000_000_000 })); + } + + const promisePay = async () => { + const payer = randArrItem(payers); + return new Promise(async resolve => { + const amount = randInt(100_000, 1_000_000); + await payer.runCommand(payCommand, { user: { user: payeeTarget }, amount: amount.toString() }); + resolve(); + }); + }; + const promiseAdd = async () => { + return new Promise(async resolve => { + await userPayee.addItemsToBank({ items: new Bank().add('Cannonball', 100) }); + resolve(); + }); + }; + + const promises: Promise[] = []; + let newBalls = 0; + for (let j = 0; j < iterations; j++) { + if (roll(addChance)) { + newBalls += 100; + promises.push(promiseAdd()); + } else { + promises.push(promisePay()); + } + } + + await Promise.all(promises); + + let totalGp = 0; + await userPayee.sync(); + await Promise.all(payers.map(u => u.sync())); + totalGp += userPayee.GP; + for (const payer of payers) totalGp += payer.GP; + + expect(totalGp).toEqual(1_000_000_000 * (payerCount + 1)); + expect(userPayee.bank.amount('Cannonball') - startingBallCount).toEqual(newBalls); + }, + { + timeout: Time.Minute * 5, + repeats + } + ); + + test( + 'GE Simulation (Payer)', + async () => { + await mockClient(); + // May as well test for the Payer also, even though so far we're solid here. + const userPayer = await createTestUser(randomSnowflake(), new Bank(bigBank), { GP: 1_000_000_000 }); + + const startingBallCount = userPayer.bank.amount('Cannonball'); + + const payees: TestUser[] = []; + for (let i = 0; i < payerCount; i++) { + payees.push(await createTestUser(randomSnowflake(), new Bank(), { GP: 1_000_000_000 })); + } + + const promisePay = async () => { + const payee = randArrItem(payees); + const payeeTarget = await globalClient.fetchUser(payee.id); + return new Promise(async resolve => { + const amount = randInt(100_000, 1_000_000); + await userPayer.runCommand(payCommand, { user: { user: payeeTarget }, amount: amount.toString() }); + resolve(); + }); + }; + const promiseAdd = async () => { + return new Promise(async resolve => { + await userPayer.addItemsToBank({ items: new Bank().add('Cannonball', 100) }); + resolve(); + }); + }; + + const promises: Promise[] = []; + let newBalls = 0; + for (let j = 0; j < iterations; j++) { + if (roll(addChance)) { + newBalls += 100; + promises.push(promiseAdd()); + } else { + promises.push(promisePay()); + } + } + + await Promise.all(promises); + + let totalGp = 0; + await userPayer.sync(); + await Promise.all(payees.map(u => u.sync())); + totalGp += userPayer.GP; + for (const payee of payees) totalGp += payee.GP; + + expect(totalGp).toEqual(1_000_000_000 * (payerCount + 1)); + expect(userPayer.bank.amount('Cannonball') - startingBallCount).toEqual(newBalls); + }, + { + timeout: Time.Minute * 5, + repeats + } + ); +}); diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index 9a7e07c9b7..8617aef368 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -22,6 +22,7 @@ vi.mock('../../src/lib/util/webhook', async () => { }); // @ts-ignore mock -globalClient.fetchUser = async () => ({ +globalClient.fetchUser = async (id: string | bigint) => ({ + id: typeof id === 'string' ? id : String(id), send: async () => {} }); From d5790e602966e98741bf364277d4bd4f0e228772 Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Fri, 2 Feb 2024 02:17:17 +0000 Subject: [PATCH 02/28] Fix Farming return string (#5630) --- src/tasks/minions/farmingActivity.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/tasks/minions/farmingActivity.ts b/src/tasks/minions/farmingActivity.ts index ea00ceed73..f638a9d50b 100644 --- a/src/tasks/minions/farmingActivity.ts +++ b/src/tasks/minions/farmingActivity.ts @@ -298,18 +298,6 @@ export const farmingTask: MinionTask = { bonusXP += Math.floor(farmingXpReceived * bonusXpMultiplier); - if (bonusXP > 0) { - infoStr.push( - `\nYou received an additional ${bonusXP.toLocaleString()} bonus XP from your farmer's outfit.` - ); - } - - if (herbloreXp > 0) { - infoStr.push( - `\nYou received ${herbloreXp.toLocaleString()} Herblore XP for cleaning the herbs during your trip.` - ); - } - const xpRes = await user.addXP({ skillName: SkillsEnum.Farming, amount: Math.floor(farmingXpReceived + bonusXP), @@ -339,6 +327,12 @@ export const farmingTask: MinionTask = { ); } + if (herbloreXp > 0) { + infoStr.push( + `\nYou received ${herbloreXp.toLocaleString()} Herblore XP for cleaning the herbs during your trip.` + ); + } + const { petDropRate } = skillingPetDropRate(user, SkillsEnum.Farming, plantToHarvest.petChance); if (plantToHarvest.seedType === 'hespori') { await user.incrementKC(Monsters.Hespori.id, patchType.lastQuantity); From 35194cc3bc9b4fc0d793ff79b4a677d381a4f64b Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:29:23 -0600 Subject: [PATCH 03/28] Combat Achievment Slayer Helms revert and imbue (#5568) --- src/lib/data/creatables/caCreatables.ts | 36 +++++++ src/lib/data/creatablesTable.txt | 12 +++ .../nightmareZoneCommand.ts | 15 +++ .../abstracted_commands/soulWarsCommand.ts | 15 +++ .../unit/snapshots/banksnapshots.test.ts.snap | 102 ++++++++++++++++++ 5 files changed, 180 insertions(+) diff --git a/src/lib/data/creatables/caCreatables.ts b/src/lib/data/creatables/caCreatables.ts index bc96d6daf6..4e9aacdcbe 100644 --- a/src/lib/data/creatables/caCreatables.ts +++ b/src/lib/data/creatables/caCreatables.ts @@ -14,6 +14,12 @@ export const caCreatables: Createable[] = [ return null; } }, + { + name: 'Revert Tztok slayer helmet', + outputItems: new Bank().add('Slayer helmet'), + inputItems: new Bank().add('Tztok slayer helmet'), + noCl: true + }, { name: 'Tztok slayer helmet (i)', inputItems: new Bank().add('Slayer helmet (i)'), @@ -25,6 +31,12 @@ export const caCreatables: Createable[] = [ return null; } }, + { + name: 'Revert Tztok slayer helmet (i)', + outputItems: new Bank().add('Slayer helmet (i)'), + inputItems: new Bank().add('Tztok slayer helmet (i)'), + noCl: true + }, // { name: 'Dragon hunter crossbow (t)', @@ -73,6 +85,12 @@ export const caCreatables: Createable[] = [ return null; } }, + { + name: 'Revert Tzkal slayer helmet', + outputItems: new Bank().add('Slayer helmet'), + inputItems: new Bank().add('Tzkal slayer helmet'), + noCl: true + }, { name: 'Tzkal slayer helmet (i)', inputItems: new Bank().add('Slayer helmet (i)'), @@ -84,6 +102,12 @@ export const caCreatables: Createable[] = [ return null; } }, + { + name: 'Revert Tzkal slayer helmet (i)', + outputItems: new Bank().add('Slayer helmet (i)'), + inputItems: new Bank().add('Tzkal slayer helmet (i)'), + noCl: true + }, // { name: 'Vampyric slayer helmet', @@ -96,6 +120,12 @@ export const caCreatables: Createable[] = [ return null; } }, + { + name: 'Revert Vampyric slayer helmet', + outputItems: new Bank().add('Slayer helmet'), + inputItems: new Bank().add('Vampyric slayer helmet'), + noCl: true + }, { name: 'Vampyric slayer helmet (i)', inputItems: new Bank().add('Slayer helmet (i)'), @@ -107,6 +137,12 @@ export const caCreatables: Createable[] = [ return null; } }, + { + name: 'Revert Vampyric slayer helmet (i)', + outputItems: new Bank().add('Slayer helmet (i)'), + inputItems: new Bank().add('Vampyric slayer helmet (i)'), + noCl: true + }, // { name: "Ghommal's avernic defender 5", diff --git a/src/lib/data/creatablesTable.txt b/src/lib/data/creatablesTable.txt index 16f7c3959a..69b54cc5fe 100644 --- a/src/lib/data/creatablesTable.txt +++ b/src/lib/data/creatablesTable.txt @@ -1811,8 +1811,12 @@ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Tztok slayer helmet │ 1x Slayer helmet │ 1x Tztok slayer helmet │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ +║ Revert Tztok slayer helmet │ 1x Tztok slayer helmet │ 1x Slayer helmet │ 0 ║ +╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Tztok slayer helmet (i) │ 1x Slayer helmet (i) │ 1x Tztok slayer helmet (i) │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ +║ Revert Tztok slayer helmet (i) │ 1x Tztok slayer helmet (i) │ 1x Slayer helmet (i) │ 0 ║ +╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Dragon hunter crossbow (t) │ 1x Dragon hunter crossbow, 1x Vorkath's head │ 1x Dragon hunter crossbow (t) │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Revert Dragon hunter crossbow (t) │ 1x Dragon hunter crossbow (t) │ 1x Dragon hunter crossbow, 1x Vorkath's head │ 0 ║ @@ -1823,12 +1827,20 @@ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Tzkal slayer helmet │ 1x Slayer helmet │ 1x Tzkal slayer helmet │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ +║ Revert Tzkal slayer helmet │ 1x Tzkal slayer helmet │ 1x Slayer helmet │ 0 ║ +╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Tzkal slayer helmet (i) │ 1x Slayer helmet (i) │ 1x Tzkal slayer helmet (i) │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ +║ Revert Tzkal slayer helmet (i) │ 1x Tzkal slayer helmet (i) │ 1x Slayer helmet (i) │ 0 ║ +╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Vampyric slayer helmet │ 1x Slayer helmet │ 1x Vampyric slayer helmet │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ +║ Revert Vampyric slayer helmet │ 1x Vampyric slayer helmet │ 1x Slayer helmet │ 0 ║ +╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Vampyric slayer helmet (i) │ 1x Slayer helmet (i) │ 1x Vampyric slayer helmet (i) │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ +║ Revert Vampyric slayer helmet (i) │ 1x Vampyric slayer helmet (i) │ 1x Slayer helmet (i) │ 0 ║ +╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Ghommal's avernic defender 5 │ 1x Avernic defender, 1x Ghommal's hilt 5 │ 1x Ghommal's avernic defender 5 │ 0 ║ ╟─────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────╢ ║ Revert Ghommal's avernic defender 5 │ 1x Ghommal's avernic defender 5 │ 1x Avernic defender, 1x Ghommal's hilt 5 │ 0 ║ diff --git a/src/mahoji/lib/abstracted_commands/nightmareZoneCommand.ts b/src/mahoji/lib/abstracted_commands/nightmareZoneCommand.ts index adaadca2b5..4b472712a3 100644 --- a/src/mahoji/lib/abstracted_commands/nightmareZoneCommand.ts +++ b/src/mahoji/lib/abstracted_commands/nightmareZoneCommand.ts @@ -140,6 +140,21 @@ export const nightmareZoneImbueables = [ output: getOSItem('Hydra slayer helmet (i)'), points: 1_250_000 }, + { + input: getOSItem('Tztok slayer helmet'), + output: getOSItem('Tztok slayer helmet (i)'), + points: 1_250_000 + }, + { + input: getOSItem('Vampyric slayer helmet'), + output: getOSItem('Vampyric slayer helmet (i)'), + points: 1_250_000 + }, + { + input: getOSItem('Tzkal slayer helmet'), + output: getOSItem('Tzkal slayer helmet (i)'), + points: 1_250_000 + }, { input: getOSItem('Salve amulet'), output: getOSItem('Salve amulet(i)'), points: 800_000 }, { input: getOSItem('Salve amulet (e)'), output: getOSItem('Salve amulet(ei)'), points: 800_000 }, { diff --git a/src/mahoji/lib/abstracted_commands/soulWarsCommand.ts b/src/mahoji/lib/abstracted_commands/soulWarsCommand.ts index a8033423cb..a6326c5a8d 100644 --- a/src/mahoji/lib/abstracted_commands/soulWarsCommand.ts +++ b/src/mahoji/lib/abstracted_commands/soulWarsCommand.ts @@ -65,6 +65,21 @@ export const soulWarsImbueables = [ output: getOSItem('Hydra slayer helmet (i)'), tokens: 500 }, + { + input: getOSItem('Tztok slayer helmet'), + output: getOSItem('Tztok slayer helmet (i)'), + tokens: 500 + }, + { + input: getOSItem('Vampyric slayer helmet'), + output: getOSItem('Vampyric slayer helmet (i)'), + tokens: 500 + }, + { + input: getOSItem('Tzkal slayer helmet'), + output: getOSItem('Tzkal slayer helmet (i)'), + tokens: 500 + }, { input: getOSItem('Salve amulet'), output: getOSItem('Salve amulet(i)'), tokens: 320 }, { input: getOSItem('Salve amulet (e)'), output: getOSItem('Salve amulet(ei)'), tokens: 320 }, { diff --git a/tests/unit/snapshots/banksnapshots.test.ts.snap b/tests/unit/snapshots/banksnapshots.test.ts.snap index e5b3869855..4452333099 100644 --- a/tests/unit/snapshots/banksnapshots.test.ts.snap +++ b/tests/unit/snapshots/banksnapshots.test.ts.snap @@ -23789,6 +23789,23 @@ exports[`OSB Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "25898": 1, + }, + "frozen": false, + }, + "name": "Revert Tztok slayer helmet", + "noCl": true, + "outputItems": Bank { + "bank": { + "11864": 1, + }, + "frozen": false, + }, + }, { "cantHaveItems": undefined, "customReq": [Function], @@ -23806,6 +23823,23 @@ exports[`OSB Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "25900": 1, + }, + "frozen": false, + }, + "name": "Revert Tztok slayer helmet (i)", + "noCl": true, + "outputItems": Bank { + "bank": { + "11865": 1, + }, + "frozen": false, + }, + }, { "cantHaveItems": undefined, "customReq": [Function], @@ -23895,6 +23929,23 @@ exports[`OSB Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "25910": 1, + }, + "frozen": false, + }, + "name": "Revert Tzkal slayer helmet", + "noCl": true, + "outputItems": Bank { + "bank": { + "11864": 1, + }, + "frozen": false, + }, + }, { "cantHaveItems": undefined, "customReq": [Function], @@ -23912,6 +23963,23 @@ exports[`OSB Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "25912": 1, + }, + "frozen": false, + }, + "name": "Revert Tzkal slayer helmet (i)", + "noCl": true, + "outputItems": Bank { + "bank": { + "11865": 1, + }, + "frozen": false, + }, + }, { "cantHaveItems": undefined, "customReq": [Function], @@ -23929,6 +23997,23 @@ exports[`OSB Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "25904": 1, + }, + "frozen": false, + }, + "name": "Revert Vampyric slayer helmet", + "noCl": true, + "outputItems": Bank { + "bank": { + "11864": 1, + }, + "frozen": false, + }, + }, { "cantHaveItems": undefined, "customReq": [Function], @@ -23946,6 +24031,23 @@ exports[`OSB Creatables 1`] = ` "frozen": false, }, }, + { + "cantHaveItems": undefined, + "inputItems": Bank { + "bank": { + "25906": 1, + }, + "frozen": false, + }, + "name": "Revert Vampyric slayer helmet (i)", + "noCl": true, + "outputItems": Bank { + "bank": { + "11865": 1, + }, + "frozen": false, + }, + }, { "cantHaveItems": undefined, "customReq": [Function], From 5530682cf40fa2cddfb5403e2678fb384ac99d7f Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:35:49 -0600 Subject: [PATCH 04/28] Balance Gauntlet times & boosts (#5566) * Balance Gauntlet times & boosts * add lower level prayers * remove redundant requirements * Normal hunllef boost fix --- .../abstracted_commands/gauntletCommand.ts | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts index a2a2677318..f506714547 100644 --- a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts +++ b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts @@ -3,8 +3,9 @@ import { calcWhatPercent, reduceNumByPercent, Time } from 'e'; import { BitField } from '../../../lib/constants'; import { getMinigameScore } from '../../../lib/settings/minigames'; +import { SkillsEnum } from '../../../lib/skilling/types'; import { GauntletOptions } from '../../../lib/types/minions'; -import { formatDuration, formatSkillRequirements } from '../../../lib/util'; +import { formatDuration, formatSkillRequirements, randomVariation } from '../../../lib/util'; import addSubTaskToActivityTask from '../../../lib/util/addSubTaskToActivityTask'; import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; @@ -18,7 +19,8 @@ const baseRequirements = { smithing: 70, herblore: 70, construction: 70, - hunter: 70 + hunter: 70, + prayer: 77 }; const standardRequirements = { @@ -27,8 +29,7 @@ const standardRequirements = { strength: 80, defence: 80, magic: 80, - ranged: 80, - prayer: 77 + ranged: 80 }; const corruptedRequirements = { @@ -37,14 +38,7 @@ const corruptedRequirements = { strength: 90, defence: 90, magic: 90, - ranged: 90, - prayer: 77, - // Skilling - cooking: 70, - farming: 70, - fishing: 70, - mining: 70, - woodcutting: 70 + ranged: 90 }; export async function gauntletCommand(user: MUser, channelID: string, type: 'corrupted' | 'normal' = 'normal') { @@ -54,6 +48,7 @@ export async function gauntletCommand(user: MUser, channelID: string, type: 'cor } const readableName = `${toTitleCase(type)} Gauntlet`; const requiredSkills = type === 'corrupted' ? corruptedRequirements : standardRequirements; + const prayLevel = user.skillLevel(SkillsEnum.Prayer); if (!user.hasSkillReqs(requiredSkills)) { return `You don't have the required stats to do the ${readableName}, you need: ${formatSkillRequirements( @@ -70,28 +65,57 @@ export async function gauntletCommand(user: MUser, channelID: string, type: 'cor return "You can't attempt the Corrupted Gauntlet, you have less than 50 normal Gauntlets completed - you would not stand a chance in the Corrupted Gauntlet!"; } - let baseLength = type === 'corrupted' ? Time.Minute * 10 : Time.Minute * 14; + // Base times for gauntlet prep. + const normPrep = Time.Minute * 8; + const corrPrep = Time.Minute * 7.5; + + // Base times for Hunllef fight. + const normHunllef = Time.Minute * 3.5; + const corrHunllef = Time.Minute * 5.5; + + let baseLength = type === 'corrupted' ? corrPrep + corrHunllef : normPrep + normHunllef; const boosts = []; - const scoreBoost = Math.min(100, calcWhatPercent(type === 'corrupted' ? corruptedKC : normalKC, 100)) / 5; + // Gauntlet prep boost + let prepBoost = Math.min(100, calcWhatPercent(corruptedKC + normalKC, 100)) / 5; + if (prepBoost > 1) { + if (type === 'corrupted') { + baseLength = reduceNumByPercent(baseLength, prepBoost); + boosts.push(`${prepBoost}% boost for experience with preparation`); + } else { + prepBoost *= 2; + baseLength = reduceNumByPercent(baseLength, prepBoost); + boosts.push(`${prepBoost}% boost for experience with preparation (2x for normal prep)`); + } + } + + // Hunllef boss fight boost + const scoreBoost = + Math.min(100, calcWhatPercent(type === 'corrupted' ? corruptedKC : normalKC + corruptedKC, 100)) / 5; if (scoreBoost > 1) { baseLength = reduceNumByPercent(baseLength, scoreBoost); - boosts.push(`${scoreBoost}% boost for experience in the minigame`); + boosts.push(`${scoreBoost}% boost for ${type === 'corrupted' ? 'Corrupted ' : ''}Hunllef KC`); } if (user.bitfield.includes(BitField.HasArcaneScroll)) { - boosts.push('3% for Augury'); - baseLength = reduceNumByPercent(baseLength, 3); + boosts.push('5% for Augury'); + baseLength = reduceNumByPercent(baseLength, 5); + } else if (prayLevel >= 45) { + boosts.push('2% for Mystic Might'); + baseLength = reduceNumByPercent(baseLength, 2); } if (user.bitfield.includes(BitField.HasDexScroll)) { - boosts.push('3% for Rigour'); - baseLength = reduceNumByPercent(baseLength, 3); + boosts.push('5% for Rigour'); + baseLength = reduceNumByPercent(baseLength, 5); + } else if (prayLevel >= 44) { + boosts.push('2% for Eagle Eye'); + baseLength = reduceNumByPercent(baseLength, 2); } - let gauntletLength = baseLength; - if (type === 'corrupted') gauntletLength *= 1.3; + // Add a 5% variance to account for randomness of gauntlet + let gauntletLength = randomVariation(baseLength, 5); const maxTripLength = calcMaxTripLength(user, 'Gauntlet'); From 27fb5d8884a52bd4fb8c79ea7299fe7ffe9473bf Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Fri, 2 Feb 2024 03:16:51 +0000 Subject: [PATCH 05/28] Add toggle for Clue Buttons (#5635) --- src/lib/clues/clueUtils.ts | 5 +++-- src/lib/constants.ts | 8 +++++++- src/lib/util/handleTripFinish.ts | 2 +- src/mahoji/commands/config.ts | 4 ++++ src/mahoji/lib/abstracted_commands/barbAssault.ts | 2 +- src/mahoji/lib/abstracted_commands/minionStatusCommand.ts | 2 +- src/mahoji/lib/abstracted_commands/openCommand.ts | 2 +- 7 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/lib/clues/clueUtils.ts b/src/lib/clues/clueUtils.ts index f5dab77393..2a19770411 100644 --- a/src/lib/clues/clueUtils.ts +++ b/src/lib/clues/clueUtils.ts @@ -1,6 +1,7 @@ import { ButtonBuilder, ButtonStyle } from 'discord.js'; import { Bank } from 'oldschooljs'; +import { BitField } from '../constants'; import { ClueTiers } from './clueTiers'; export function getClueScoresFromOpenables(openableScores: Bank, mutate = false) { @@ -23,9 +24,9 @@ export function deduplicateClueScrolls({ loot, currentBank }: { loot: Bank; curr return newLoot; } -export function buildClueButtons(loot: Bank | null, perkTier: number) { +export function buildClueButtons(loot: Bank | null, perkTier: number, user: MUser) { const components: ButtonBuilder[] = []; - if (loot && perkTier > 1) { + if (loot && perkTier > 1 && !user.bitfield.includes(BitField.DisableClueButtons)) { const clueReceived = ClueTiers.filter(tier => loot.amount(tier.scrollID) > 0); components.push( ...clueReceived.map(clue => diff --git a/src/lib/constants.ts b/src/lib/constants.ts index fe3bd7c7d7..a6fb172899 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -258,7 +258,8 @@ export enum BitField { UsedFrozenTablet = 34, CleanHerbsFarming = 35, SelfGamblingLocked = 36, - DisabledFarmingReminders = 37 + DisabledFarmingReminders = 37, + DisableClueButtons = 38 } interface BitFieldData { @@ -338,6 +339,11 @@ export const BitFieldData: Record = { name: 'Disable Farming Reminders', protected: false, userConfigurable: true + }, + [BitField.DisableClueButtons]: { + name: 'Disable Clue Buttons', + protected: false, + userConfigurable: true } } as const; diff --git a/src/lib/util/handleTripFinish.ts b/src/lib/util/handleTripFinish.ts index 3443e4a268..3f30f8bf3f 100644 --- a/src/lib/util/handleTripFinish.ts +++ b/src/lib/util/handleTripFinish.ts @@ -135,7 +135,7 @@ export async function handleTripFinish( const casketReceived = loot ? ClueTiers.find(i => loot?.has(i.id)) : undefined; if (casketReceived) components.push(makeOpenCasketButton(casketReceived)); if (perkTier > PerkTier.One) { - components.push(...buildClueButtons(loot, perkTier)); + components.push(...buildClueButtons(loot, perkTier, user)); const birdHousedetails = await calculateBirdhouseDetails(user.id); if (birdHousedetails.isReady && !user.bitfield.includes(BitField.DisableBirdhouseRunButton)) components.push(makeBirdHouseTripButton()); diff --git a/src/mahoji/commands/config.ts b/src/mahoji/commands/config.ts index daa2fca7c5..7ee13e7e3c 100644 --- a/src/mahoji/commands/config.ts +++ b/src/mahoji/commands/config.ts @@ -119,6 +119,10 @@ const toggles: UserConfigToggle[] = [ { name: 'Disable farming reminders', bit: BitField.DisabledFarmingReminders + }, + { + name: 'Disable Clue Buttons', + bit: BitField.DisableClueButtons } ]; diff --git a/src/mahoji/lib/abstracted_commands/barbAssault.ts b/src/mahoji/lib/abstracted_commands/barbAssault.ts index 2802e26d63..db43a0fdd4 100644 --- a/src/mahoji/lib/abstracted_commands/barbAssault.ts +++ b/src/mahoji/lib/abstracted_commands/barbAssault.ts @@ -226,7 +226,7 @@ export async function barbAssaultGambleCommand( const { itemsAdded, previousCL } = await user.addItemsToBank({ items: loot, collectionLog: true }); const perkTier = user.perkTier(); - const components: ButtonBuilder[] = buildClueButtons(loot, perkTier); + const components: ButtonBuilder[] = buildClueButtons(loot, perkTier, user); let response: Awaited = { content: `You spent ${( diff --git a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts index ba9b700905..5d44b902f8 100644 --- a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts +++ b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts @@ -134,7 +134,7 @@ export async function minionStatusCommand(user: MUser): Promise bank.has(t.scrollID)) .reverse() .slice(0, 3)) { diff --git a/src/mahoji/lib/abstracted_commands/openCommand.ts b/src/mahoji/lib/abstracted_commands/openCommand.ts index 45d554b33e..c61a5c06c5 100644 --- a/src/mahoji/lib/abstracted_commands/openCommand.ts +++ b/src/mahoji/lib/abstracted_commands/openCommand.ts @@ -127,7 +127,7 @@ async function finalizeOpening({ .join(', '); const perkTier = user.perkTier(); - const components: ButtonBuilder[] = buildClueButtons(loot, perkTier); + const components: ButtonBuilder[] = buildClueButtons(loot, perkTier, user); let response: Awaited = { files: [image.file], From 5689067fab00848c2553646a627fe4a7d6a3bd62 Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:42:12 +0000 Subject: [PATCH 06/28] Add toggle to Auto Slay Button (#5634) --- src/lib/constants.ts | 8 +++++++- src/lib/util/handleTripFinish.ts | 2 +- src/mahoji/commands/config.ts | 4 ++++ src/mahoji/lib/abstracted_commands/minionStatusCommand.ts | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a6fb172899..abb085ff39 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -259,7 +259,8 @@ export enum BitField { CleanHerbsFarming = 35, SelfGamblingLocked = 36, DisabledFarmingReminders = 37, - DisableClueButtons = 38 + DisableClueButtons = 38, + DisableAutoSlayButton = 39 } interface BitFieldData { @@ -344,6 +345,11 @@ export const BitFieldData: Record = { name: 'Disable Clue Buttons', protected: false, userConfigurable: true + }, + [BitField.DisableAutoSlayButton]: { + name: 'Disable Auto Slay Button', + protected: false, + userConfigurable: true } } as const; diff --git a/src/lib/util/handleTripFinish.ts b/src/lib/util/handleTripFinish.ts index 3f30f8bf3f..a1290432b5 100644 --- a/src/lib/util/handleTripFinish.ts +++ b/src/lib/util/handleTripFinish.ts @@ -149,7 +149,7 @@ export async function handleTripFinish( ['MonsterKilling', 'Inferno', 'FightCaves'].includes(data.type) ) { components.push(makeNewSlayerTaskButton()); - } else { + } else if (!user.bitfield.includes(BitField.DisableAutoSlayButton)) { components.push(makeAutoSlayButton()); } if (loot?.has('Seed pack')) { diff --git a/src/mahoji/commands/config.ts b/src/mahoji/commands/config.ts index 7ee13e7e3c..ac07d20f93 100644 --- a/src/mahoji/commands/config.ts +++ b/src/mahoji/commands/config.ts @@ -49,6 +49,10 @@ const toggles: UserConfigToggle[] = [ name: 'Disable Birdhouse Run Button', bit: BitField.DisableBirdhouseRunButton }, + { + name: 'Disable Auto Slay Button', + bit: BitField.DisableAutoSlayButton + }, { name: 'Disable Ash Sanctifier', bit: BitField.DisableAshSanctifier diff --git a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts index 5d44b902f8..ff2d0ce1de 100644 --- a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts +++ b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts @@ -103,7 +103,7 @@ export async function minionStatusCommand(user: MUser): Promise Date: Sun, 4 Feb 2024 16:00:16 -0800 Subject: [PATCH 07/28] Fix gear presets - double equip bug, and edit improvements (#5647) * Fix edit gear preset so it doesn't wipe everything not specified * Dont allow gearpresets to equip 2h and weapon/shield, and force unequip if so. * Add / update tests * Allow removal of items via gearpresets edit * Make global presets autocomplete filter by input. --- src/lib/MUser.ts | 18 ++++ src/mahoji/commands/gearpresets.ts | 84 +++++++++++++++---- .../lib/abstracted_commands/gearCommands.ts | 4 + src/mahoji/lib/mahojiCommandOptions.ts | 4 +- .../integration/commands/gearPresets.test.ts | 80 ++++++++++++++++++ tests/integration/commands/gifts.test.ts | 1 - tests/integration/gearFixing.test.ts | 79 ++++++++++++----- 7 files changed, 226 insertions(+), 44 deletions(-) create mode 100644 tests/integration/commands/gearPresets.test.ts diff --git a/src/lib/MUser.ts b/src/lib/MUser.ts index 00a9fbd689..a7cd714ad8 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -824,6 +824,24 @@ GROUP BY data->>'clueID';`); async validateEquippedGear() { let itemsUnequippedAndRefunded = new Bank(); for (const [gearSetupName, gearSetup] of Object.entries(this.gear) as [GearSetupType, GearSetup][]) { + if (gearSetup['2h'] !== null) { + if (gearSetup.weapon?.item) { + const { refundBank } = await this.forceUnequip( + gearSetupName, + EquipmentSlot.Weapon, + '2h Already equipped' + ); + itemsUnequippedAndRefunded.add(refundBank); + } + if (gearSetup.shield?.item) { + const { refundBank } = await this.forceUnequip( + gearSetupName, + EquipmentSlot.Shield, + '2h Already equipped' + ); + itemsUnequippedAndRefunded.add(refundBank); + } + } for (const slot of Object.values(EquipmentSlot)) { const item = gearSetup[slot]; if (!item) continue; diff --git a/src/mahoji/commands/gearpresets.ts b/src/mahoji/commands/gearpresets.ts index 1521861c63..c61a394153 100644 --- a/src/mahoji/commands/gearpresets.ts +++ b/src/mahoji/commands/gearpresets.ts @@ -1,3 +1,4 @@ +import { GearPreset } from '@prisma/client'; import { ApplicationCommandOptionType, CommandRunOptions } from 'mahoji'; import { CommandOption } from 'mahoji/dist/lib/types'; import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; @@ -5,9 +6,9 @@ import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; import { production } from '../../config'; import { ParsedCustomEmojiWithGroups } from '../../lib/constants'; import { generateGearImage } from '../../lib/gear/functions/generateGearImage'; -import { GearSetupType, GearSetupTypes } from '../../lib/gear/types'; +import { GearSetup, GearSetupType, GearSetupTypes } from '../../lib/gear/types'; import { prisma } from '../../lib/settings/prisma'; -import { Gear, globalPresets } from '../../lib/structures/Gear'; +import { defaultGear, Gear, globalPresets } from '../../lib/structures/Gear'; import { cleanString, isValidGearSetup, isValidNickname, stringMatches } from '../../lib/util'; import { emojiServers } from '../../lib/util/cachedUserIDs'; import { getItem } from '../../lib/util/getOSItem'; @@ -23,15 +24,42 @@ type InputGear = Partial>; type ParsedInputGear = Partial>; function parseInputGear(inputGear: InputGear) { let gear: ParsedInputGear = {}; + let remove: EquipmentSlot[] = []; for (const [key, val] of Object.entries(inputGear)) { + if (val?.toLowerCase() === 'none') { + remove.push(key as EquipmentSlot); + continue; + } const item = getItem(val); if (item && item.equipment?.slot === key) { gear[key as EquipmentSlot] = item.id; } } - return gear; + return { gear, remove }; } +export function gearPresetToGear(preset: GearPreset): GearSetup { + function gearItem(val: null | number) { + if (val === null) return null; + return { + item: val, + quantity: 1 + }; + } + const newGear = { ...defaultGear }; + newGear.head = gearItem(preset.head); + newGear.neck = gearItem(preset.neck); + newGear.body = gearItem(preset.body); + newGear.legs = gearItem(preset.legs); + newGear.cape = gearItem(preset.cape); + newGear['2h'] = gearItem(preset.two_handed); + newGear.hands = gearItem(preset.hands); + newGear.feet = gearItem(preset.feet); + newGear.shield = gearItem(preset.shield); + newGear.weapon = gearItem(preset.weapon); + newGear.ring = gearItem(preset.ring); + return newGear; +} export async function createOrEditGearSetup( user: MUser, setupToCopy: GearSetupType | undefined, @@ -65,8 +93,18 @@ export async function createOrEditGearSetup( return `The maximum amount of gear presets you can have is ${max}, you can unlock more slots by becoming a patron!`; } - const parsedInputGear = parseInputGear(gearInput); - let gearSetup = setupToCopy ? user.gear[setupToCopy] : null; + const { gear: parsedInputGear, remove: forceRemove } = parseInputGear(gearInput); + let gearSetup: Gear | GearSetup | null = null; + if (setupToCopy) { + gearSetup = user.gear[setupToCopy]; + } else if (isUpdating) { + gearSetup = gearPresetToGear(userPresets.find(pre => pre.name === name)!); + } + + // This is required to enable removal of items while editing + for (const slot of forceRemove) { + if (gearSetup !== null) gearSetup[slot] = null; + } if (emoji) { const cachedEmoji = globalClient.emojis.cache.get(emoji); @@ -80,23 +118,28 @@ export async function createOrEditGearSetup( } const gearData = { - head: gearSetup?.head?.item ?? parsedInputGear.head ?? null, - neck: gearSetup?.neck?.item ?? parsedInputGear.neck ?? null, - body: gearSetup?.body?.item ?? parsedInputGear.body ?? null, - legs: gearSetup?.legs?.item ?? parsedInputGear.legs ?? null, - cape: gearSetup?.cape?.item ?? parsedInputGear.cape ?? null, - two_handed: gearSetup?.['2h']?.item ?? parsedInputGear['2h'] ?? null, - hands: gearSetup?.hands?.item ?? parsedInputGear.hands ?? null, - feet: gearSetup?.feet?.item ?? parsedInputGear.feet ?? null, - shield: gearSetup?.shield?.item ?? parsedInputGear.shield ?? null, - weapon: gearSetup?.weapon?.item ?? parsedInputGear.weapon ?? null, - ring: gearSetup?.ring?.item ?? parsedInputGear.ring ?? null, - ammo: gearSetup?.ammo?.item ?? parsedInputGear.ammo ?? null, + head: parsedInputGear.head ?? gearSetup?.head?.item ?? null, + neck: parsedInputGear.neck ?? gearSetup?.neck?.item ?? null, + body: parsedInputGear.body ?? gearSetup?.body?.item ?? null, + legs: parsedInputGear.legs ?? gearSetup?.legs?.item ?? null, + cape: parsedInputGear.cape ?? gearSetup?.cape?.item ?? null, + two_handed: parsedInputGear['2h'] ?? gearSetup?.['2h']?.item ?? null, + hands: parsedInputGear.hands ?? gearSetup?.hands?.item ?? null, + feet: parsedInputGear.feet ?? gearSetup?.feet?.item ?? null, + shield: parsedInputGear.shield ?? gearSetup?.shield?.item ?? null, + weapon: parsedInputGear.weapon ?? gearSetup?.weapon?.item ?? null, + ring: parsedInputGear.ring ?? gearSetup?.ring?.item ?? null, + ammo: parsedInputGear.ammo ?? gearSetup?.ammo?.item ?? null, ammo_qty: gearSetup?.ammo?.quantity ?? null, emoji_id: emoji ?? undefined, pinned_setup: !pinned_setup || pinned_setup === 'reset' ? undefined : pinned_setup }; + if (gearData.two_handed !== null) { + gearData.shield = null; + gearData.weapon = null; + } + const preset = await prisma.gearPreset.upsert({ where: { user_id_name: { @@ -128,7 +171,7 @@ function makeSlotOption(slot: EquipmentSlot): CommandOption { description: `The item you want to put in the ${slot} slot in this gear setup.`, required: false, autocomplete: async (value: string) => { - return ( + const matchingItems = ( value ? allEquippableItems.filter(i => i.name.toLowerCase().includes(value.toLowerCase())) : allEquippableItems @@ -136,6 +179,11 @@ function makeSlotOption(slot: EquipmentSlot): CommandOption { .filter(i => i.equipment?.slot === slot) .slice(0, 20) .map(i => ({ name: i.name, value: i.name })); + if (!value || 'none'.includes(value.toLowerCase())) { + matchingItems.unshift({ name: 'None', value: 'none' }); + if (matchingItems.length > 20) matchingItems.pop(); + } + return matchingItems; } }; } diff --git a/src/mahoji/lib/abstracted_commands/gearCommands.ts b/src/mahoji/lib/abstracted_commands/gearCommands.ts index b9b3989bac..c910a2b46e 100644 --- a/src/mahoji/lib/abstracted_commands/gearCommands.ts +++ b/src/mahoji/lib/abstracted_commands/gearCommands.ts @@ -39,6 +39,10 @@ export async function gearPresetEquipCommand(user: MUser, gearSetup: string, pre return "You don't have a gear preset with that name."; } const preset = (userPreset ?? globalPreset) as GearPreset; + if (preset.two_handed !== null) { + preset.weapon = null; + preset.shield = null; + } // Checks the preset to make sure the user has the required stats for every item in the preset for (const gearItemId of Object.values(preset)) { diff --git a/src/mahoji/lib/mahojiCommandOptions.ts b/src/mahoji/lib/mahojiCommandOptions.ts index 4536876848..65cb33d51a 100644 --- a/src/mahoji/lib/mahojiCommandOptions.ts +++ b/src/mahoji/lib/mahojiCommandOptions.ts @@ -141,9 +141,9 @@ export const gearPresetOption: CommandOption = { } }); return presets - .filter(i => (!value ? true : i.name.toLowerCase().includes(value.toLowerCase()))) .map(i => ({ name: i.name, value: i.name })) - .concat(globalPresets.map(i => ({ name: `${i.name} (Global)`, value: i.name }))); + .concat(globalPresets.map(i => ({ name: `${i.name} (Global)`, value: i.name }))) + .filter(i => (!value ? true : i.name.toLowerCase().includes(value.toLowerCase()))); } }; diff --git a/tests/integration/commands/gearPresets.test.ts b/tests/integration/commands/gearPresets.test.ts new file mode 100644 index 0000000000..4d514ae44e --- /dev/null +++ b/tests/integration/commands/gearPresets.test.ts @@ -0,0 +1,80 @@ +import { randInt } from 'e'; +import { describe, expect, test, vi } from 'vitest'; + +import { prisma } from '../../../src/lib/settings/prisma'; +import itemID from '../../../src/lib/util/itemID'; +import { gearPresetsCommand } from '../../../src/mahoji/commands/gearpresets'; +import { createTestUser } from '../util'; + +vi.mock('../../../src/lib/util', async () => { + const actual: any = await vi.importActual('../../../src/lib/util'); + return { + ...actual, + cryptoRand: (min: number, max: number) => randInt(min, max) + }; +}); + +describe('Gear Presets Command', async () => { + const user = await createTestUser(); + + test('Create preset with 2h', async () => { + await user.runCommand(gearPresetsCommand, { + create: { + name: 'Test2h', + '2h': 'Bronze 2h sword', + weapon: 'Bronze dagger', + shield: 'Bronze kiteshield', + feet: 'Bronze boots', + head: 'Bronze full helm' + } + }); + + const gpResult = await prisma.gearPreset.findFirst({ where: { user_id: user.id, name: 'test2h' } }); + // Verify row exists: + expect(gpResult).toBeTruthy(); + if (!gpResult) return false; + + // Make sure row is as expected: + expect(gpResult.head).toEqual(itemID('Bronze full helm')); + expect(gpResult.feet).toEqual(itemID('Bronze boots')); + expect(gpResult.weapon).toBeNull(); + expect(gpResult.shield).toBeNull(); + expect(gpResult.two_handed).toEqual(itemID('Bronze 2h sword')); + }); + + test('Test edit gearpreset', async () => { + // Generate gearPreset to edit: + await user.runCommand(gearPresetsCommand, { + create: { + name: 'TestEdit', + weapon: 'Bronze dagger', + shield: 'Bronze kiteshield', + feet: 'Bronze boots', + head: 'Bronze full helm' + } + }); + + const gpResult = await prisma.gearPreset.findFirst({ where: { user_id: user.id, name: 'testedit' } }); + // Verify row exists: + expect(gpResult).toBeTruthy(); + if (!gpResult) return false; + + await user.runCommand(gearPresetsCommand, { + edit: { gear_preset: 'TestEdit', weapon: 'Ghrazi rapier', feet: 'None' } + }); + + const gpEditResult = await prisma.gearPreset.findFirst({ where: { user_id: user.id, name: 'testedit' } }); + + // Verify row found: + expect(gpEditResult).toBeTruthy(); + if (!gpEditResult) return false; + + // Make sure row is as expected: + expect(gpEditResult.head).toEqual(itemID('Bronze full helm')); + expect(gpEditResult.feet).toBeNull(); + expect(gpEditResult.weapon).toEqual(itemID('Ghrazi rapier')); + expect(gpEditResult.shield).toEqual(itemID('Bronze kiteshield')); + expect(gpEditResult.hands).toBeNull(); + expect(gpEditResult.ammo).toBeNull(); + }); +}); diff --git a/tests/integration/commands/gifts.test.ts b/tests/integration/commands/gifts.test.ts index 3cfdd7e279..c942b5649e 100644 --- a/tests/integration/commands/gifts.test.ts +++ b/tests/integration/commands/gifts.test.ts @@ -2,7 +2,6 @@ import { Bank } from 'oldschooljs'; import { beforeEach, describe, test } from 'vitest'; import { giftCommand } from '../../../src/mahoji/commands/gift'; -import { minigamesCommand } from '../../../src/mahoji/commands/minigames'; import { createTestUser, mockClient } from '../util'; describe('Gift Command', async () => { diff --git a/tests/integration/gearFixing.test.ts b/tests/integration/gearFixing.test.ts index 2a913149b1..9534ef0a4b 100644 --- a/tests/integration/gearFixing.test.ts +++ b/tests/integration/gearFixing.test.ts @@ -1,40 +1,73 @@ import deepEqual from 'deep-equal'; import { Bank } from 'oldschooljs'; -import { test } from 'vitest'; +import { describe, test } from 'vitest'; import { defaultGear, Gear } from '../../src/lib/structures/Gear'; import { assert } from '../../src/lib/util'; import itemID from '../../src/lib/util/itemID'; import { createTestUser, mockClient } from './util'; -test('Gear Fixing', async () => { - await mockClient(); - const user = await createTestUser(); +describe('Gear Fixing', async () => { + test('Basic tests', async () => { + await mockClient(); + const user = await createTestUser(); - const expectedRefund = new Bank().add('Twisted bow', 5).add('Dragon boots'); + const expectedRefund = new Bank().add('Twisted bow', 5).add('Dragon boots'); - const fixedGear = new Gear().raw(); - fixedGear.shield = { item: itemID('Twisted bow'), quantity: 5 }; - fixedGear.feet = { item: itemID('Dragon boots'), quantity: 1 }; - fixedGear.body = { item: itemID('Bronze platebody'), quantity: 1 }; + const fixedGear = new Gear().raw(); + fixedGear.shield = { item: itemID('Twisted bow'), quantity: 5 }; + fixedGear.feet = { item: itemID('Dragon boots'), quantity: 1 }; + fixedGear.body = { item: itemID('Bronze platebody'), quantity: 1 }; - await user.update({ - gear_melee: fixedGear as any + await user.update({ + gear_melee: fixedGear as any + }); + + const { itemsUnequippedAndRefunded } = await user.validateEquippedGear(); + + assert( + itemsUnequippedAndRefunded.equals(expectedRefund), + `Expected ${itemsUnequippedAndRefunded} to equal ${expectedRefund}` + ); + + assert(user.bank.equals(expectedRefund), `Expected ${user.bank} to equal ${expectedRefund}`); + + assert( + deepEqual(user.gear.melee.raw(), { + ...defaultGear, + body: { item: itemID('Bronze platebody'), quantity: 1 } + }) + ); }); + test('Multiple 2h + 1h equipped tests', async () => { + await mockClient(); + const user = await createTestUser(); - const { itemsUnequippedAndRefunded } = await user.validateEquippedGear(); + const expectedRefund = new Bank().add('Bronze dagger').add('Bronze kiteshield'); - assert( - itemsUnequippedAndRefunded.equals(expectedRefund), - `Expected ${itemsUnequippedAndRefunded} to equal ${expectedRefund}` - ); + const fixedGear = new Gear().raw(); + fixedGear['2h'] = { item: itemID('Bronze 2h sword'), quantity: 1 }; + fixedGear.shield = { item: itemID('Bronze kiteshield'), quantity: 1 }; + fixedGear.weapon = { item: itemID('Bronze dagger'), quantity: 1 }; - assert(user.bank.equals(expectedRefund), `Expected ${user.bank} to equal ${expectedRefund}`); + await user.update({ + gear_melee: fixedGear as any + }); - assert( - deepEqual(user.gear.melee.raw(), { - ...defaultGear, - body: { item: itemID('Bronze platebody'), quantity: 1 } - }) - ); + const { itemsUnequippedAndRefunded } = await user.validateEquippedGear(); + + assert( + itemsUnequippedAndRefunded.equals(expectedRefund), + `Expected ${itemsUnequippedAndRefunded} to equal ${expectedRefund}` + ); + + assert(user.bank.equals(expectedRefund), `Expected ${user.bank} to equal ${expectedRefund}`); + + assert( + deepEqual(user.gear.melee.raw(), { + ...defaultGear, + '2h': { item: itemID('Bronze 2h sword'), quantity: 1 } + }) + ); + }); }); From d7c332140b19824824294ebf646a8b73dd859cb8 Mon Sep 17 00:00:00 2001 From: themrrobert <10122432+themrrobert@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:03:22 -0800 Subject: [PATCH 08/28] Fix 'permanent busy' bug that occurs after deleting a pending interaction (#5607) --- src/lib/util/interactionHelpers.ts | 1 + src/lib/util/interactionReply.ts | 38 +++++++++++++++++++----------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/lib/util/interactionHelpers.ts b/src/lib/util/interactionHelpers.ts index 18ba1adb40..f9c899aa05 100644 --- a/src/lib/util/interactionHelpers.ts +++ b/src/lib/util/interactionHelpers.ts @@ -21,6 +21,7 @@ export async function interactionReplyGetDuration( const response = await interactionReply(interaction, { content: prompt, components }); + if (response === undefined) return false; try { const selection = await response.awaitMessageComponent({ filter: i => i.user.id === interaction.user.id, diff --git a/src/lib/util/interactionReply.ts b/src/lib/util/interactionReply.ts index a8bcbc80f6..f32c6a551d 100644 --- a/src/lib/util/interactionReply.ts +++ b/src/lib/util/interactionReply.ts @@ -2,22 +2,36 @@ import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; import { ButtonInteraction, ChatInputCommandInteraction, + DiscordAPIError, Interaction, InteractionReplyOptions, + InteractionResponse, + Message, RepliableInteraction } from 'discord.js'; import { SILENT_ERROR } from '../constants'; import { logErrorForInteraction } from './logError'; -export function interactionReply(interaction: RepliableInteraction, response: string | InteractionReplyOptions) { +export async function interactionReply(interaction: RepliableInteraction, response: string | InteractionReplyOptions) { + let i: Promise | Promise | undefined = undefined; if (interaction.replied) { - return interaction.followUp(response); + i = interaction.followUp(response); + } else if (interaction.deferred) { + i = interaction.editReply(response); + } else { + i = interaction.reply(response); } - if (interaction.deferred) { - return interaction.editReply(response); + try { + await i; + return i; + } catch (e: any) { + if (e instanceof DiscordAPIError && e.code !== 10_008) { + // 10_008 is unknown message, e.g. if someone deletes the message before it's replied to. + logErrorForInteraction(e, interaction); + } + return undefined; } - return interaction.reply(response); } export function deferInteraction(interaction: ButtonInteraction | ChatInputCommandInteraction) { @@ -32,20 +46,16 @@ export async function handleInteractionError(err: unknown, interaction: Interact // For silent errors, just return and do nothing. Users could see an error. if (err instanceof Error && err.message === SILENT_ERROR) return; + // If DiscordAPIError #10008, that means someone deleted the message, we don't need to log this. + if (err instanceof DiscordAPIError && err.code === 10_008) { + return; + } + // If this isn't a UserError, its something we need to just log and know about to fix it. // Or if its not repliable, we should never be erroring here. if (!(err instanceof UserError) || !interaction.isRepliable()) { return logErrorForInteraction(err, interaction); } - // If it can be replied too, and hasn't been replied too, reply with the error. - // If it has already been replied too, log the UserError so we can know where this mix-up is coming from. - if (interaction.replied) { - return logErrorForInteraction( - new Error(`Tried to respond with '${err.message}' to this interaction, but it was already replied too.`), - interaction - ); - } - await interactionReply(interaction, err.message); } From e19758b90a0efe349107167cd071d5c912be134c Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Sun, 4 Feb 2024 18:06:14 -0600 Subject: [PATCH 09/28] Change how Black Mask drops to fix CL issues (Remove from loot filter) (#5639) --- src/lib/slayer/slayerUtil.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/lib/slayer/slayerUtil.ts b/src/lib/slayer/slayerUtil.ts index 1b500b6fa6..b2b4248edc 100644 --- a/src/lib/slayer/slayerUtil.ts +++ b/src/lib/slayer/slayerUtil.ts @@ -364,7 +364,6 @@ export function hasSlayerUnlock( } const filterLootItems = resolveItems([ - 'Black mask (10)', "Hydra's eye", "Hydra's fang", "Hydra's heart", @@ -379,13 +378,12 @@ const bludgeonPieces = resolveItems(['Bludgeon claw', 'Bludgeon spine', 'Bludgeo export function filterLootReplace(myBank: Bank, myLoot: Bank) { // Order: Fang, eye, heart. - const numBlackMask = myLoot.amount('Black mask (10)'); let numHydraEyes = myLoot.amount("Hydra's eye"); numHydraEyes += myLoot.amount("Hydra's fang"); numHydraEyes += myLoot.amount("Hydra's heart"); const numDarkTotemBases = myLoot.amount('Dark totem base'); const numBludgeonPieces = myLoot.amount('Bludgeon claw'); - if (!numBludgeonPieces && !numDarkTotemBases && !numHydraEyes && !numBlackMask) { + if (!numBludgeonPieces && !numDarkTotemBases && !numHydraEyes) { return { bankLoot: myLoot, clLoot: myLoot }; } @@ -393,13 +391,6 @@ export function filterLootReplace(myBank: Bank, myLoot: Bank) { const myClLoot = new Bank(myLoot.bank); - if (numBlackMask) { - for (let x = 0; x < numBlackMask; x++) { - myLoot.add('Black mask'); - myClLoot.add('Black mask (10)'); - } - } - const combinedBank = new Bank(myBank).add(myLoot); if (numBludgeonPieces) { for (let x = 0; x < numBludgeonPieces; x++) { From 1c1d2c56381176a19870e8ef0d820d733b83794e Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Wed, 7 Feb 2024 01:27:43 -0600 Subject: [PATCH 10/28] Add data for Personal XP from Tears of Guthix (#5660) --- .../lib/abstracted_commands/statCommand.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/mahoji/lib/abstracted_commands/statCommand.ts b/src/mahoji/lib/abstracted_commands/statCommand.ts index 4ac6cc766f..7b422798cf 100644 --- a/src/mahoji/lib/abstracted_commands/statCommand.ts +++ b/src/mahoji/lib/abstracted_commands/statCommand.ts @@ -3,7 +3,7 @@ import { sumArr, Time } from 'e'; import { CommandResponse } from 'mahoji/dist/lib/structures/ICommand'; import { Bank, Monsters } from 'oldschooljs'; import { SkillsEnum } from 'oldschooljs/dist/constants'; -import { ItemBank } from 'oldschooljs/dist/meta/types'; +import { ItemBank, SkillsScore } from 'oldschooljs/dist/meta/types'; import { TOBRooms } from 'oldschooljs/dist/simulation/misc/TheatreOfBlood'; import { toKMB } from 'oldschooljs/dist/util'; @@ -11,6 +11,7 @@ import { ClueTiers } from '../../../lib/clues/clueTiers'; import { getClueScoresFromOpenables } from '../../../lib/clues/clueUtils'; import { Emoji, PerkTier } from '../../../lib/constants'; import { calcCLDetails, isCLItem } from '../../../lib/data/Collections'; +import { skillEmoji } from '../../../lib/data/emojis'; import { getBankBgById } from '../../../lib/minions/data/bankBackgrounds'; import killableMonsters from '../../../lib/minions/data/killableMonsters'; import { RandomEvents } from '../../../lib/randomEvents'; @@ -952,6 +953,27 @@ GROUP BY "bankBackground";`); ).toLocaleString()}** XP from using the Ash Sanctifier.`; } }, + { + name: 'Personal XP gained from Tears of Guthix', + perkTierNeeded: PerkTier.Four, + run: async (user: MUser) => { + const result = await prisma.$queryRawUnsafe( + `SELECT skill, + SUM(xp) AS total_xp + FROM xp_gains + WHERE source = 'TearsOfGuthix' + AND user_id = ${BigInt(user.id)} + GROUP BY skill` + ); + + return `**Personal XP gained from Tears of Guthix**\n${result + .map( + (i: any) => + `${skillEmoji[i.skill as keyof typeof skillEmoji] as keyof SkillsScore} ${toKMB(i.total_xp)}` + ) + .join('\n')}`; + } + }, { name: 'Bird Eggs Offered', perkTierNeeded: null, From 1c328cfec02cdd37870e100c8b8fd1fa317e36c3 Mon Sep 17 00:00:00 2001 From: themrrobert <10122432+themrrobert@users.noreply.github.com> Date: Wed, 7 Feb 2024 05:59:24 -0800 Subject: [PATCH 11/28] `/drop` improvement - Double check drop message fix (#5664) --- src/lib/util/smallUtils.ts | 7 +++++++ src/mahoji/commands/drop.ts | 20 +++++++++++--------- src/mahoji/commands/sell.ts | 10 ++++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/lib/util/smallUtils.ts b/src/lib/util/smallUtils.ts index 68b0221541..f0cc081665 100644 --- a/src/lib/util/smallUtils.ts +++ b/src/lib/util/smallUtils.ts @@ -332,3 +332,10 @@ export function containsBlacklistedWord(str: string): boolean { } return false; } + +export function ellipsize(str: string, maxLen: number = 2000) { + if (str.length > maxLen) { + return `${str.substring(0, maxLen - 3)}...`; + } + return str; +} diff --git a/src/mahoji/commands/drop.ts b/src/mahoji/commands/drop.ts index 88f4d3ffec..3840391128 100644 --- a/src/mahoji/commands/drop.ts +++ b/src/mahoji/commands/drop.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, CommandRunOptions } from 'mahoji'; import { ClueTiers } from '../../lib/clues/clueTiers'; -import { itemNameFromID } from '../../lib/util'; +import { ellipsize, itemNameFromID, returnStringOrFile } from '../../lib/util'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { parseBank } from '../../lib/util/parseStringBank'; import { updateBankSetting } from '../../lib/util/updateBankSetting'; @@ -66,23 +66,25 @@ export const dropCommand: OSBMahojiCommand = { ].flat(1); const doubleCheckItems = itemsToDoubleCheck.filter(f => bank.has(f)); + await handleMahojiConfirmation( + interaction, + `${user}, are you sure you want to drop ${ellipsize( + bank.toString(), + 1800 + )}? This is irreversible, and you will lose the items permanently.` + ); if (doubleCheckItems.length > 0) { await handleMahojiConfirmation( interaction, - `${user}, some of the items you are dropping look valuable, are you *really* sure you want to drop them? **${doubleCheckItems + `${user}, some of the items you are dropping are on your **favorites** or look valuable, are you *really* sure you want to drop them?\n**${doubleCheckItems .map(itemNameFromID) - .join(', ')}**` - ); - } else { - await handleMahojiConfirmation( - interaction, - `${user}, are you sure you want to drop ${bank}? This is irreversible, and you will lose the items permanently.` + .join(', ')}**\n\nDropping: ${ellipsize(bank.toString(), 1000)}` ); } await user.removeItemsFromBank(bank); updateBankSetting('dropped_items', bank); - return `Dropped ${bank}.`; + return returnStringOrFile(`Dropped ${bank}.`); } }; diff --git a/src/mahoji/commands/sell.ts b/src/mahoji/commands/sell.ts index 994fbd3587..c79979b8f6 100644 --- a/src/mahoji/commands/sell.ts +++ b/src/mahoji/commands/sell.ts @@ -7,7 +7,7 @@ import { Item, ItemBank } from 'oldschooljs/dist/meta/types'; import { MAX_INT_JAVA } from '../../lib/constants'; import { prisma } from '../../lib/settings/prisma'; import { NestBoxesTable } from '../../lib/simulation/misc'; -import { itemID, toKMB } from '../../lib/util'; +import { itemID, returnStringOrFile, toKMB } from '../../lib/util'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { parseBank } from '../../lib/util/parseStringBank'; import { updateBankSetting } from '../../lib/util/updateBankSetting'; @@ -234,8 +234,10 @@ export const sellCommand: OSBMahojiCommand = { prisma.botItemSell.createMany({ data: botItemSellData }) ]); - return `Sold ${bankToSell} for **${totalPrice.toLocaleString()}gp (${toKMB(totalPrice)})**${ - user.isIronman ? ' (General store price)' : ` (${taxRatePercent}% below market price)` - }.`; + return returnStringOrFile( + `Sold ${bankToSell} for **${totalPrice.toLocaleString()}gp (${toKMB(totalPrice)})**${ + user.isIronman ? ' (General store price)' : ` (${taxRatePercent}% below market price)` + }.` + ); } }; From 11cd63d03216fd9ba31cec546d91fbcba74b58a2 Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Fri, 9 Feb 2024 00:47:05 -0600 Subject: [PATCH 12/28] Fix CA command being case sensitive (#5671) --- src/mahoji/commands/ca.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mahoji/commands/ca.ts b/src/mahoji/commands/ca.ts index 4ad29b3f41..eb1c298a1c 100644 --- a/src/mahoji/commands/ca.ts +++ b/src/mahoji/commands/ca.ts @@ -140,7 +140,7 @@ export const caCommand: OSBMahojiCommand = { if (selectedMonster) { const tasksForSelectedMonster = allCombatAchievementTasks.filter( - task => task.monster === selectedMonster + task => task.monster.toLowerCase() === selectedMonster!.toLowerCase() ); if (tasksForSelectedMonster.length === 0) From 72042906f2345dcb91fa7c387ce79277acd7bd4d Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Fri, 9 Feb 2024 20:11:51 -0600 Subject: [PATCH 13/28] Remove extra TOB clue scrolls (#5672) --- src/lib/simulation/tob.ts | 13 ++++++++++++- src/tasks/minions/minigames/tobActivity.ts | 9 ++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/lib/simulation/tob.ts b/src/lib/simulation/tob.ts index 1c789f1fd1..6993e2a0da 100644 --- a/src/lib/simulation/tob.ts +++ b/src/lib/simulation/tob.ts @@ -4,6 +4,7 @@ import { Bank, LootTable } from 'oldschooljs'; import { LootBank } from 'oldschooljs/dist/meta/types'; import { convertLootBanksToItemBanks, JSONClone } from 'oldschooljs/dist/util'; +import { BOT_TYPE } from '../constants'; import { TOBRooms } from '../data/tob'; import { assert } from '../util/logError'; @@ -50,7 +51,6 @@ const HardModeUniqueTable = new LootTable() .add('Avernic defender hilt', 1, 7); const NonUniqueTable = new LootTable() - .tertiary(25, 'Clue scroll (elite)') .add('Vial of blood', [50, 60], 2) .add('Death rune', [500, 600]) .add('Blood rune', [500, 600]) @@ -93,6 +93,10 @@ export class TheatreOfBloodClass { if (deaths.length === TOBRooms.length) { return new Bank().add('Cabbage'); } + + if (BOT_TYPE === 'BSO') { + NonUniqueTable.tertiary(25, 'Clue scroll (elite)'); + } const loot = new Bank(); for (let i = 0; i < 3; i++) { loot.add(NonUniqueTable.roll()); @@ -106,6 +110,13 @@ export class TheatreOfBloodClass { // Add HM Tertiary drops: dust / kits loot.add(HardModeExtraTable.roll()); } + + if (BOT_TYPE === 'OSB') { + if (roll(25)) { + loot.add('Clue scroll (elite)'); + } + } + let petChance = isHardMode ? 500 : 650; if (member.numDeaths > 0) { petChance *= member.numDeaths; diff --git a/src/tasks/minions/minigames/tobActivity.ts b/src/tasks/minions/minigames/tobActivity.ts index f25fe8ac75..8c080390c3 100644 --- a/src/tasks/minions/minigames/tobActivity.ts +++ b/src/tasks/minions/minigames/tobActivity.ts @@ -3,7 +3,7 @@ import { roll, shuffleArr } from 'e'; import { Bank } from 'oldschooljs'; import { drawChestLootImage } from '../../../lib/bankImage'; -import { Emoji, Events } from '../../../lib/constants'; +import { BOT_TYPE, Emoji, Events } from '../../../lib/constants'; import { tobMetamorphPets } from '../../../lib/data/CollectionsExport'; import { TOBRooms, TOBUniques, TOBUniquesToAnnounce } from '../../../lib/data/tob'; import { trackLoot } from '../../../lib/lootTrack'; @@ -141,6 +141,13 @@ export const tobTask: MinionTask = { // Refund initial 100k entry cost userLoot.add('Coins', 100_000); + // Remove elite clue scroll if OSB & user has one in bank + if (BOT_TYPE === 'OSB') { + if (user.owns('Clue scroll (elite)')) { + userLoot.remove('Clue scroll (elite)', 1); + } + } + // Add this raids loot to the raid's total loot: totalLoot.add(userLoot); From 2abdaff9cbccd020dffe24e49f8fa8db460c2bf5 Mon Sep 17 00:00:00 2001 From: TURBO Date: Sat, 10 Feb 2024 03:35:09 -0600 Subject: [PATCH 14/28] Fix only lowercase searches in CL issue (#5678) --- src/mahoji/commands/cl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mahoji/commands/cl.ts b/src/mahoji/commands/cl.ts index 62d5a2ea6b..b24fefd496 100644 --- a/src/mahoji/commands/cl.ts +++ b/src/mahoji/commands/cl.ts @@ -39,7 +39,7 @@ export const collectionLogCommand: OSBMahojiCommand = { ]; }) .flat(3) - ].filter(i => (!value ? true : i.name.toLowerCase().includes(value))); + ].filter(i => (!value ? true : i.name.toLowerCase().includes(value.toLowerCase()))); } }, { From a32cd7441b4b8d4ec9ff8fd3d3cfe85b272231fb Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:22:55 +0000 Subject: [PATCH 15/28] Add /testpotato setslayertask (#5682) --- src/lib/slayer/tasks/index.ts | 2 + src/mahoji/commands/testpotato.ts | 100 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/lib/slayer/tasks/index.ts b/src/lib/slayer/tasks/index.ts index a514c6c844..b33c3dccc0 100644 --- a/src/lib/slayer/tasks/index.ts +++ b/src/lib/slayer/tasks/index.ts @@ -18,3 +18,5 @@ export const allSlayerTasks: AssignableSlayerTask[] = [ ...vannakaTasks, ...duradelTasks ]; + +export const allSlayerMonsters = allSlayerTasks.map(m => m.monster); diff --git a/src/mahoji/commands/testpotato.ts b/src/mahoji/commands/testpotato.ts index 11e9f306af..3006ae867a 100644 --- a/src/mahoji/commands/testpotato.ts +++ b/src/mahoji/commands/testpotato.ts @@ -24,6 +24,10 @@ 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 { slayerMasterChoices } from '../../lib/slayer/constants'; +import { slayerMasters } from '../../lib/slayer/slayerMasters'; +import { getUsersCurrentSlayerInfo } from '../../lib/slayer/slayerUtil'; +import { allSlayerMonsters } from '../../lib/slayer/tasks'; import { stringMatches } from '../../lib/util'; import { calcDropRatesFromBankWithoutUniques } from '../../lib/util/calcDropRatesFromBank'; import { @@ -514,6 +518,46 @@ export const testPotatoCommand: OSBMahojiCommand | null = production } } ] + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'setslayertask', + description: 'Set slayer task.', + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'master', + description: 'The master you wish to set your task.', + required: true, + choices: slayerMasterChoices + }, + { + type: ApplicationCommandOptionType.String, + name: 'monster', + description: 'The monster you want to set your task as.', + required: true, + autocomplete: async value => { + const filteredMonsters = [...new Set(allSlayerMonsters)].filter(monster => { + if (!value) return true; + return [monster.name.toLowerCase(), ...monster.aliases].some(aliases => + aliases.includes(value.toLowerCase()) + ); + }); + return filteredMonsters.map(monster => ({ + name: monster.name, + value: monster.name + })); + } + }, + { + type: ApplicationCommandOptionType.Integer, + name: 'quantity', + description: 'The task quantity you want to assign.', + required: false, + min_value: 0, + max_value: 1000 + } + ] } ], run: async ({ @@ -534,6 +578,7 @@ export const testPotatoCommand: OSBMahojiCommand | null = production set?: { qp?: number; all_ca_tasks?: boolean }; check?: { monster_droprates?: string }; bingo_tools?: { start_bingo: string }; + setslayertask?: { master: string; monster: string; quantity: number }; }>) => { if (production) { logError('Test command ran in production', { userID: userID.toString() }); @@ -832,6 +877,61 @@ ${droprates.join('\n')}`), return userGrowingProgressStr((await getFarmingInfo(userID)).patchesDetailed); } + if (options.setslayertask) { + const user = await mUserFetch(userID); + const usersTask = await getUsersCurrentSlayerInfo(user.id); + + const { monster, master } = options.setslayertask; + + const selectedMonster = allSlayerMonsters.find(m => stringMatches(m.name, monster)); + const selectedMaster = slayerMasters.find( + sm => stringMatches(master, sm.name) || sm.aliases.some(alias => stringMatches(master, alias)) + ); + + // Set quantity to 50 if user doesn't assign a quantity + const quantity = options.setslayertask?.quantity ?? 50; + + const assignedTask = selectedMaster!.tasks.find(m => m.monster.id === selectedMonster?.id)!; + + if (!selectedMaster) return 'Invalid slayer master.'; + if (!selectedMonster) return 'Invalid monster.'; + if (!assignedTask) return `${selectedMaster.name} can not assign ${selectedMonster.name}.`; + + // Update an existing slayer task for the user + if (usersTask.currentTask?.id) { + await prisma.slayerTask.update({ + where: { + id: usersTask.currentTask?.id + }, + data: { + quantity, + quantity_remaining: quantity, + slayer_master_id: selectedMaster.id, + monster_id: selectedMonster.id, + skipped: false + } + }); + } else { + // Create a new slayer task for the user + await prisma.slayerTask.create({ + data: { + user_id: user.id, + quantity, + quantity_remaining: quantity, + slayer_master_id: selectedMaster.id, + monster_id: selectedMonster.id, + skipped: false + } + }); + } + + await user.update({ + slayer_last_task: selectedMonster.id + }); + + return `You set your slayer task to ${selectedMonster.name} using ${selectedMaster.name}.`; + } + return 'Nothin!'; } }; From 4af59edd2c59f3a2d8ef7feb9f8adea32c2f1d1c Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Mon, 12 Feb 2024 20:58:53 -0600 Subject: [PATCH 16/28] Fix notifications for shades of morton cl (#5688) --- src/lib/data/Collections.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/data/Collections.ts b/src/lib/data/Collections.ts index 469215519e..c3dd7a8bd3 100644 --- a/src/lib/data/Collections.ts +++ b/src/lib/data/Collections.ts @@ -143,7 +143,7 @@ function kcProg(mon: Monster): FormatProgressFunction { } function mgProg(minigameName: MinigameName): FormatProgressFunction { - return ({ minigames }) => `${minigames[minigameName]} KC`; + return ({ minigames }) => `${minigames[minigameName]} Completions`; } function skillProg(skillName: SkillsEnum): FormatProgressFunction { @@ -807,7 +807,7 @@ export const allCollectionLogs: ICollection = { "Shades of Mort'ton": { items: shadesOfMorttonCL, isActivity: true, - fmtProg: () => '0 KC' + fmtProg: mgProg('shades_of_morton') }, 'Soul Wars': { alias: ['soul wars', 'sw'], From 326764168ab8f86940cb89f35524acb9cc6f3f37 Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Mon, 12 Feb 2024 21:05:37 -0600 Subject: [PATCH 17/28] Toggle for wilderness high peak time warning (#5411) --- src/lib/constants.ts | 8 +++++++- src/mahoji/commands/config.ts | 4 ++++ src/mahoji/lib/abstracted_commands/minionKill.ts | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index abb085ff39..50b28b0fac 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -260,7 +260,8 @@ export enum BitField { SelfGamblingLocked = 36, DisabledFarmingReminders = 37, DisableClueButtons = 38, - DisableAutoSlayButton = 39 + DisableAutoSlayButton = 39, + DisableHighPeakTimeWarning = 40 } interface BitFieldData { @@ -350,6 +351,11 @@ export const BitFieldData: Record = { name: 'Disable Auto Slay Button', protected: false, userConfigurable: true + }, + [BitField.DisableHighPeakTimeWarning]: { + name: 'Disable Wilderness High Peak Time Warning', + protected: false, + userConfigurable: true } } as const; diff --git a/src/mahoji/commands/config.ts b/src/mahoji/commands/config.ts index ac07d20f93..6c22feff83 100644 --- a/src/mahoji/commands/config.ts +++ b/src/mahoji/commands/config.ts @@ -127,6 +127,10 @@ const toggles: UserConfigToggle[] = [ { name: 'Disable Clue Buttons', bit: BitField.DisableClueButtons + }, + { + name: 'Disable wilderness high peak time warning', + bit: BitField.DisableHighPeakTimeWarning } ]; diff --git a/src/mahoji/lib/abstracted_commands/minionKill.ts b/src/mahoji/lib/abstracted_commands/minionKill.ts index c1f71fdebb..12b1068142 100644 --- a/src/mahoji/lib/abstracted_commands/minionKill.ts +++ b/src/mahoji/lib/abstracted_commands/minionKill.ts @@ -15,7 +15,7 @@ import { Bank, Monsters } from 'oldschooljs'; import { MonsterAttribute } from 'oldschooljs/dist/meta/monsterData'; import { itemID } from 'oldschooljs/dist/util'; -import { PeakTier, PvMMethod } from '../../../lib/constants'; +import { BitField, PeakTier, PvMMethod } from '../../../lib/constants'; import { Eatables } from '../../../lib/data/eatables'; import { getSimilarItems } from '../../../lib/data/similarItems'; import { checkUserCanUseDegradeableItem, degradeablePvmBoostItems, degradeItem } from '../../../lib/degradeableItems'; @@ -689,7 +689,7 @@ export async function minionKillCommand( break; } } - if (wildyPeak?.peakTier === PeakTier.High) { + if (wildyPeak?.peakTier === PeakTier.High && !user.bitfield.includes(BitField.DisableHighPeakTimeWarning)) { if (interaction) { await handleMahojiConfirmation( interaction, From dbf35dde59fe852d0cba0af5e3e5779eeb6c72c7 Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:17:28 +0000 Subject: [PATCH 18/28] Allow Inferno KC to impact Fightcaves (#5689) --- .../abstracted_commands/fightCavesCommand.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts index aaf951f314..784f7ff254 100644 --- a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts +++ b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts @@ -4,6 +4,7 @@ import { Bank, Monsters } from 'oldschooljs'; import TzTokJad from 'oldschooljs/dist/simulation/monsters/special/TzTokJad'; import { itemID } from 'oldschooljs/dist/util'; +import { getMinigameScore } from '../../../lib/settings/minigames'; import { getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; import { FightCavesActivityTaskOptions } from '../../../lib/types/minions'; import { formatDuration } from '../../../lib/util'; @@ -24,7 +25,9 @@ async function determineDuration(user: MUser): Promise<[number, string]> { // Reduce time based on KC const jadKC = await user.getKC(TzTokJad.id); - const percentIncreaseFromKC = Math.min(50, jadKC); + const zukKC = await getMinigameScore(user.id, 'inferno'); + const experienceKC = jadKC + zukKC * 3; + const percentIncreaseFromKC = Math.min(50, experienceKC); baseTime = reduceNumByPercent(baseTime, percentIncreaseFromKC); debugStr += `${percentIncreaseFromKC}% from KC`; @@ -43,9 +46,12 @@ async function determineDuration(user: MUser): Promise<[number, string]> { return [baseTime, debugStr]; } -function determineChanceOfDeathPreJad(user: MUser, attempts: number) { +function determineChanceOfDeathPreJad(user: MUser, attempts: number, hasInfernoKC: boolean) { let deathChance = Math.max(14 - attempts * 2, 5); + // If user has killed inferno, give them the lowest chance of death pre Jad. + if (hasInfernoKC) deathChance = 5; + // -4% Chance of dying before Jad if you have SGS. if (user.hasEquipped(itemID('Saradomin godsword'))) { deathChance -= 4; @@ -54,8 +60,12 @@ function determineChanceOfDeathPreJad(user: MUser, attempts: number) { return deathChance; } -function determineChanceOfDeathInJad(attempts: number) { - const chance = Math.floor(100 - (Math.log(attempts) / Math.log(Math.sqrt(15))) * 50); +function determineChanceOfDeathInJad(attempts: number, hasInfernoKC: boolean) { + let chance = Math.floor(100 - (Math.log(attempts) / Math.log(Math.sqrt(15))) * 50); + + if (hasInfernoKC) { + chance /= 1.5; + } // Chance of death cannot be 100% or <5%. return Math.max(Math.min(chance, 99), 5); @@ -98,11 +108,14 @@ export async function fightCavesCommand(user: MUser, channelID: string): Command const { fight_caves_attempts: attempts } = await user.fetchStats({ fight_caves_attempts: true }); - const jadDeathChance = determineChanceOfDeathInJad(attempts); - const preJadDeathChance = determineChanceOfDeathPreJad(user, attempts); + const jadKC = await user.getKC(TzTokJad.id); + const zukKC = await getMinigameScore(user.id, 'inferno'); + const hasInfernoKC = zukKC > 0; + + const jadDeathChance = determineChanceOfDeathInJad(attempts, hasInfernoKC); + const preJadDeathChance = determineChanceOfDeathPreJad(user, attempts, hasInfernoKC); const usersRangeStats = user.gear.range.stats; - const jadKC = await user.getKC(TzTokJad.id); duration += (randInt(1, 5) * duration) / 100; @@ -148,6 +161,7 @@ export async function fightCavesCommand(user: MUser, channelID: string): Command **Boosts:** ${debugStr} **Range Attack Bonus:** ${usersRangeStats.attack_ranged} **Jad KC:** ${jadKC} +**Zuk KC:** ${zukKC} **Attempts:** ${attempts} **Removed from your bank:** ${fightCavesCost}`, From 8cd741b8d9cf0d66d941a51a81e58b23ce5ba00e Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Mon, 19 Feb 2024 01:51:04 -0600 Subject: [PATCH 19/28] Update random events (#5695) --- src/lib/randomEvents.ts | 73 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/src/lib/randomEvents.ts b/src/lib/randomEvents.ts index 061257c0c9..c769b4896e 100644 --- a/src/lib/randomEvents.ts +++ b/src/lib/randomEvents.ts @@ -39,18 +39,36 @@ export const zombieOutfit = resolveItems([ export const mimeOutfit = resolveItems(['Mime mask', 'Mime top', 'Mime legs', 'Mime gloves', 'Mime boots']); +// Used by Mysterious Old man, Pillory, Rick Turpentine +const randomEventTable = new LootTable() + .add('Uncut sapphire', 1, 32) + .add('Uncut emerald', 1, 16) + .add('Coins', 80, 10) + .add('Coins', 160, 10) + .add('Coins', 320, 10) + .add('Coins', 480, 10) + .add('Coins', 640, 10) + .add('Uncut ruby', 1, 8) + .add('Coins', 240, 6) + .add('Cosmic talisman', 1, 4) + .add('Uncut diamond', 2, 2) + .add('Tooth half of key', 1, 1) + .add('Tooth half of key', 1, 1); + +// https://oldschool.runescape.wiki/w/Random_events#List_of_random_events +// Missing: Evil Bob, Jekyll and Hyde, Maze, Prison Pete export const RandomEvents: RandomEvent[] = [ { id: 1, name: 'Bee keeper', outfit: beekeeperOutfit, - loot: new LootTable().add('Coins', [20, 60]).add('Flax', [1, 27]) + loot: new LootTable().add('Coins', [16, 36]).add('Flax', [1, 27]) }, { id: 2, name: 'Drill Demon', outfit: camoOutfit, - loot: new LootTable().every('Coins', 500) + loot: new LootTable().every('Genie lamp') }, { id: 3, @@ -60,8 +78,8 @@ export const RandomEvents: RandomEvent[] = [ { id: 4, name: 'Freaky Forester', - loot: new LootTable().every('Coins', 500), - outfit: lederhosenOutfit + outfit: lederhosenOutfit, + loot: new LootTable().every('Genie lamp') }, { id: 5, @@ -71,8 +89,8 @@ export const RandomEvents: RandomEvent[] = [ { id: 6, name: 'Gravedigger', - loot: new LootTable().every('Coins', 500), - outfit: zombieOutfit + outfit: zombieOutfit, + loot: new LootTable().every('Genie lamp') }, { id: 7, @@ -82,8 +100,8 @@ export const RandomEvents: RandomEvent[] = [ { id: 8, name: 'Mime', - loot: new LootTable().every('Coins', 500), - outfit: mimeOutfit + outfit: mimeOutfit, + loot: new LootTable().every('Genie lamp') }, { id: 9, @@ -129,6 +147,45 @@ export const RandomEvents: RandomEvent[] = [ .add('Spinach roll') .add('Tooth half of key') .add('Loop half of key') + }, + { + id: 14, + name: 'Count Check', + loot: new LootTable().every('Genie lamp') + }, + { + id: 15, + name: 'Evil twin', + loot: new LootTable() + .add('Uncut sapphire', [2, 4]) + .add('Uncut emerald', [2, 4]) + .add('Uncut ruby', [2, 4]) + .add('Uncut diamond', [2, 4]) + }, + { + id: 16, + name: 'Mysterious Old Man', + loot: randomEventTable.add('Kebab', 1, 16).add('Spinach roll', 1, 14) + }, + { + id: 17, + name: 'Pillory', + loot: randomEventTable + }, + { + id: 18, + name: 'Pinball', + loot: new LootTable().add('Sapphire', 5, 3).add('Emerald', 5, 3).add('Ruby', 5, 3).add('Diamond', 2, 1) + }, + { + id: 19, + name: 'Rick Turpentine', + loot: randomEventTable.add('Kebab', 1, 16).add('Spinach roll', 1, 14) + }, + { + id: 20, + name: 'Strange plant', + loot: new LootTable().every('Strange fruit') } ]; From 7dbda44d6f8bf52d2d507f726c2df5fed2f8ae76 Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Mon, 19 Feb 2024 07:55:57 +0000 Subject: [PATCH 20/28] Update Slayer Task Weightings (#5690) Reviewed all the slayer task weightings and matched them to the wiki, removed duplicated spiritualranger tasks when spiritualmager tasks are there. --- src/lib/slayer/tasks/chaeldarTasks.ts | 14 -------------- src/lib/slayer/tasks/duradelTasks.ts | 17 +--------------- src/lib/slayer/tasks/konarTasks.ts | 6 +++--- src/lib/slayer/tasks/mazchnaTasks.ts | 16 +++++++-------- src/lib/slayer/tasks/nieveTasks.ts | 15 +------------- src/lib/slayer/tasks/turaelTasks.ts | 28 +++++++++++++-------------- src/lib/slayer/tasks/vannakaTasks.ts | 15 +------------- 7 files changed, 28 insertions(+), 83 deletions(-) diff --git a/src/lib/slayer/tasks/chaeldarTasks.ts b/src/lib/slayer/tasks/chaeldarTasks.ts index 53da8688e5..4717015acb 100644 --- a/src/lib/slayer/tasks/chaeldarTasks.ts +++ b/src/lib/slayer/tasks/chaeldarTasks.ts @@ -438,20 +438,6 @@ export const chaeldarTasks: AssignableSlayerTask[] = [ unlocked: true, dontAssign: true }, - { - monster: Monsters.SpiritualRanger, - amount: [110, 170], - - weight: 12, - monsters: [Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id, Monsters.SpiritualMage.id], - levelRequirements: { - slayer: 60 - }, - combatLevel: 60, - slayerLevel: 63, - questPoints: 3, - unlocked: true - }, { monster: Monsters.MountainTroll, amount: [110, 170], diff --git a/src/lib/slayer/tasks/duradelTasks.ts b/src/lib/slayer/tasks/duradelTasks.ts index 296deacdd7..12ee193008 100644 --- a/src/lib/slayer/tasks/duradelTasks.ts +++ b/src/lib/slayer/tasks/duradelTasks.ts @@ -379,7 +379,7 @@ export const duradelTasks: AssignableSlayerTask[] = [ monster: Monsters.SpiritualMage, amount: [110, 170], - weight: 12, + weight: 7, monsters: [Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id, Monsters.SpiritualMage.id], extendedAmount: [180, 250], extendedUnlockId: SlayerTaskUnlocksEnum.SpiritualFervour, @@ -392,21 +392,6 @@ export const duradelTasks: AssignableSlayerTask[] = [ unlocked: true, dontAssign: true }, - { - monster: Monsters.SpiritualRanger, - amount: [130, 200], - weight: 7, - monsters: [Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id, Monsters.SpiritualMage.id], - extendedAmount: [180, 250], - extendedUnlockId: SlayerTaskUnlocksEnum.SpiritualFervour, - levelRequirements: { - slayer: 60 - }, - combatLevel: 60, - slayerLevel: 63, - questPoints: 3, - unlocked: true - }, { monster: Monsters.SteelDragon, amount: [10, 20], diff --git a/src/lib/slayer/tasks/konarTasks.ts b/src/lib/slayer/tasks/konarTasks.ts index 86a13b269a..da41593d7b 100644 --- a/src/lib/slayer/tasks/konarTasks.ts +++ b/src/lib/slayer/tasks/konarTasks.ts @@ -105,7 +105,7 @@ export const konarTasks: AssignableSlayerTask[] = [ { monster: Monsters.BlueDragon, amount: [120, 170], - weight: 8, + weight: 4, monsters: [Monsters.BlueDragon.id, Monsters.BabyBlueDragon.id, Monsters.BrutalBlueDragon.id], combatLevel: 65, questPoints: 34, @@ -204,7 +204,7 @@ export const konarTasks: AssignableSlayerTask[] = [ monster: Monsters.FossilIslandWyvernSpitting, amount: [15, 30], - weight: 9, + weight: 5, monsters: [ Monsters.FossilIslandWyvernAncient.id, Monsters.FossilIslandWyvernLongTailed.id, @@ -381,7 +381,7 @@ export const konarTasks: AssignableSlayerTask[] = [ { monster: Monsters.SteelDragon, amount: [30, 50], - weight: 7, + weight: 5, monsters: [Monsters.SteelDragon.id], levelRequirements: killableMonsters.find(k => k.id === Monsters.SteelDragon.id)!.levelRequirements, extendedAmount: [40, 60], diff --git a/src/lib/slayer/tasks/mazchnaTasks.ts b/src/lib/slayer/tasks/mazchnaTasks.ts index 5e92285f82..2eb078c7c3 100644 --- a/src/lib/slayer/tasks/mazchnaTasks.ts +++ b/src/lib/slayer/tasks/mazchnaTasks.ts @@ -88,14 +88,6 @@ export const mazchnaTasks: AssignableSlayerTask[] = [ questPoints: 1, unlocked: true }, - { - monster: Monsters.Lizard, - amount: [40, 70], - weight: 8, - monsters: [Monsters.Lizard.id, Monsters.SmallLizard.id, Monsters.DesertLizard.id, Monsters.SulphurLizard.id], - slayerLevel: 22, - unlocked: true - }, { monster: Monsters.GuardDog, amount: [40, 70], @@ -185,6 +177,14 @@ export const mazchnaTasks: AssignableSlayerTask[] = [ questPoints: 4, unlocked: true }, + { + monster: Monsters.Lizard, + amount: [40, 70], + weight: 8, + monsters: [Monsters.Lizard.id, Monsters.SmallLizard.id, Monsters.DesertLizard.id, Monsters.SulphurLizard.id], + slayerLevel: 22, + unlocked: true + }, { monster: Monsters.Mogre, amount: [40, 70], diff --git a/src/lib/slayer/tasks/nieveTasks.ts b/src/lib/slayer/tasks/nieveTasks.ts index 9a8565bd85..7fe148d304 100644 --- a/src/lib/slayer/tasks/nieveTasks.ts +++ b/src/lib/slayer/tasks/nieveTasks.ts @@ -358,7 +358,7 @@ export const nieveTasks: AssignableSlayerTask[] = [ monster: Monsters.SpiritualMage, amount: [110, 170], - weight: 12, + weight: 6, monsters: [Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id, Monsters.SpiritualMage.id], levelRequirements: { slayer: 60 @@ -369,19 +369,6 @@ export const nieveTasks: AssignableSlayerTask[] = [ unlocked: true, dontAssign: true }, - { - monster: Monsters.SpiritualRanger, - amount: [120, 185], - weight: 6, - monsters: [Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id, Monsters.SpiritualMage.id], - levelRequirements: { - slayer: 60 - }, - combatLevel: 60, - slayerLevel: 63, - questPoints: 3, - unlocked: true - }, { monster: Monsters.SteelDragon, amount: [30, 60], diff --git a/src/lib/slayer/tasks/turaelTasks.ts b/src/lib/slayer/tasks/turaelTasks.ts index b5c92ed0c0..1aa36e7c3a 100644 --- a/src/lib/slayer/tasks/turaelTasks.ts +++ b/src/lib/slayer/tasks/turaelTasks.ts @@ -21,6 +21,20 @@ export const turaelTasks: AssignableSlayerTask[] = [ combatLevel: 5, unlocked: true }, + { + monster: Monsters.BlackBear, + amount: [15, 50], + weight: 7, + monsters: [ + Monsters.BlackBear.id, + Monsters.GrizzlyBearCub.id, + Monsters.BearCub.id, + Monsters.GrizzlyBear.id, + Monsters.Callisto.id + ], + combatLevel: 13, + unlocked: true + }, { monster: Monsters.Bird, amount: [15, 50], @@ -38,20 +52,6 @@ export const turaelTasks: AssignableSlayerTask[] = [ ], unlocked: true }, - { - monster: Monsters.BlackBear, - amount: [15, 50], - weight: 7, - monsters: [ - Monsters.BlackBear.id, - Monsters.GrizzlyBearCub.id, - Monsters.BearCub.id, - Monsters.GrizzlyBear.id, - Monsters.Callisto.id - ], - combatLevel: 13, - unlocked: true - }, { monster: Monsters.CaveBug, amount: [10, 20], diff --git a/src/lib/slayer/tasks/vannakaTasks.ts b/src/lib/slayer/tasks/vannakaTasks.ts index 1410e43438..f3b7e1bc62 100644 --- a/src/lib/slayer/tasks/vannakaTasks.ts +++ b/src/lib/slayer/tasks/vannakaTasks.ts @@ -481,7 +481,7 @@ export const vannakaTasks: AssignableSlayerTask[] = [ monster: Monsters.SpiritualMage, amount: [110, 170], - weight: 12, + weight: 8, monsters: [Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id, Monsters.SpiritualMage.id], levelRequirements: { slayer: 60 @@ -492,19 +492,6 @@ export const vannakaTasks: AssignableSlayerTask[] = [ unlocked: true, dontAssign: true }, - { - monster: Monsters.SpiritualRanger, - amount: [60, 120], - weight: 8, - monsters: [Monsters.SpiritualRanger.id, Monsters.SpiritualWarrior.id, Monsters.SpiritualMage.id], - levelRequirements: { - slayer: 60 - }, - combatLevel: 60, - slayerLevel: 63, - questPoints: 3, - unlocked: true - }, { monster: Monsters.TerrorDog, amount: [20, 45], From 389747ff08ee23b227bed1f909e9fb0f0ceae3d3 Mon Sep 17 00:00:00 2001 From: Jonesy <69014816+nwjgit@users.noreply.github.com> Date: Mon, 19 Feb 2024 02:19:37 -0600 Subject: [PATCH 21/28] Ghommal's lucky penny, Lower vial of blood cost (#5686) --- src/lib/degradeableItems.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/lib/degradeableItems.ts b/src/lib/degradeableItems.ts index c97b4944c2..5ad9f64d04 100644 --- a/src/lib/degradeableItems.ts +++ b/src/lib/degradeableItems.ts @@ -1,3 +1,4 @@ +import { percentChance } from 'e'; import { Bank } from 'oldschooljs'; import { Item } from 'oldschooljs/dist/meta/types'; import Monster from 'oldschooljs/dist/structures/Monster'; @@ -211,7 +212,7 @@ export const degradeableItems: DegradeableItem[] = [ setup: 'melee', aliases: ['scythe of vitur'], chargeInput: { - cost: new Bank().add('Blood rune', 300).add('Vial of blood').freeze(), + cost: new Bank().add('Blood rune', 200).add('Vial of blood').freeze(), charges: 100 }, unchargedItem: getOSItem('Scythe of vitur (uncharged)'), @@ -297,6 +298,17 @@ export async function degradeItem({ const degItem = degradeableItems.find(i => i.item === item); if (!degItem) throw new Error('Invalid degradeable item'); + // 5% chance to not consume a charge when Ghommal's lucky penny is equipped + let pennyReduction = 0; + if (user.hasEquipped("Ghommal's lucky penny")) { + for (let i = 0; i < chargesToDegrade; i++) { + if (percentChance(5)) { + pennyReduction++; + } + } + } + chargesToDegrade -= pennyReduction; + const currentCharges = user.user[degItem.settingsKey]; assert(typeof currentCharges === 'number'); const newCharges = Math.floor(currentCharges - chargesToDegrade); @@ -353,7 +365,11 @@ export async function degradeItem({ const chargesAfter = user.user[degItem.settingsKey]; assert(typeof chargesAfter === 'number' && chargesAfter > 0); return { - userMessage: `Your ${item.name} degraded by ${chargesToDegrade} charges, and now has ${chargesAfter} remaining.` + userMessage: `Your ${ + item.name + } degraded by ${chargesToDegrade} charges, and now has ${chargesAfter} remaining.${ + pennyReduction > 0 ? ` Your Ghommal's lucky penny saved ${pennyReduction} charges` : '' + }` }; } From 01acb1114be7ce6322f9edc6754e280a173f4efb Mon Sep 17 00:00:00 2001 From: themrrobert <10122432+themrrobert@users.noreply.github.com> Date: Mon, 19 Feb 2024 07:46:25 -0800 Subject: [PATCH 22/28] Always show item names when opening (#5525) --- src/mahoji/lib/abstracted_commands/openCommand.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mahoji/lib/abstracted_commands/openCommand.ts b/src/mahoji/lib/abstracted_commands/openCommand.ts index c61a5c06c5..5d23e02eca 100644 --- a/src/mahoji/lib/abstracted_commands/openCommand.ts +++ b/src/mahoji/lib/abstracted_commands/openCommand.ts @@ -115,7 +115,8 @@ async function finalizeOpening({ ? `Loot from ${cost.amount(openables[0].openedItem.id)}x ${openables[0].name}` : 'Loot From Opening', user, - previousCL + previousCL, + mahojiFlags: ['show_names'] }); if (loot.has('Coins')) { From 7b554ac3909d4ecd37512c02d4972fa88644c330 Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Thu, 22 Feb 2024 06:15:57 +0000 Subject: [PATCH 23/28] Fix CoX grammar in the title in various places (#5707) Removes the incorrect apostrophe from the Chambers of Xeric name in numerous places. Closes #5631 --- src/lib/data/Collections.ts | 2 +- src/lib/data/cox.ts | 4 ++-- src/lib/settings/minigames.ts | 4 ++-- src/lib/util/minionStatus.ts | 2 +- tests/unit/snapshots/clsnapshots.test.ts.snap | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/data/Collections.ts b/src/lib/data/Collections.ts index c3dd7a8bd3..51126cbb27 100644 --- a/src/lib/data/Collections.ts +++ b/src/lib/data/Collections.ts @@ -502,7 +502,7 @@ export const allCollectionLogs: ICollection = { }, Raids: { activities: { - "Chamber's of Xeric": { + 'Chambers of Xeric': { alias: ChambersOfXeric.aliases, kcActivity: { Default: async (_, minigameScores) => diff --git a/src/lib/data/cox.ts b/src/lib/data/cox.ts index 363d56a78a..56dbe93fb8 100644 --- a/src/lib/data/cox.ts +++ b/src/lib/data/cox.ts @@ -263,10 +263,10 @@ export async function checkCoxTeam(users: MUser[], cm: boolean, quantity: number for (const user of users) { const { total } = calculateUserGearPercents(user); if (total < 20) { - return "Your gear is terrible! You do not stand a chance in the Chamber's of Xeric."; + return 'Your gear is terrible! You do not stand a chance in the Chambers of Xeric.'; } if (!hasMinRaidsRequirements(user)) { - return `${user.usernameOrMention} doesn't meet the stat requirements to do the Chamber's of Xeric.`; + return `${user.usernameOrMention} doesn't meet the stat requirements to do the Chambers of Xeric.`; } if (cm) { if (users.length === 1 && !user.hasEquippedOrInBank('Twisted bow')) { diff --git a/src/lib/settings/minigames.ts b/src/lib/settings/minigames.ts index 29838a353a..74f395aca3 100644 --- a/src/lib/settings/minigames.ts +++ b/src/lib/settings/minigames.ts @@ -97,12 +97,12 @@ export const Minigames: readonly BotMinigame[] = [ column: 'castle_wars' }, { - name: "Chamber's of Xeric", + name: 'Chambers of Xeric', aliases: ['cox', 'raid1', 'raids1', 'chambers', 'xeric'], column: 'raids' }, { - name: "Chamber's of Xeric - Challenge Mode", + name: 'Chambers of Xeric - Challenge Mode', aliases: ['coxcm', 'raid1cm', 'raids1cm', 'chamberscm', 'xericcm'], column: 'raids_challenge_mode' }, diff --git a/src/lib/util/minionStatus.ts b/src/lib/util/minionStatus.ts index 73671add3a..e770be0684 100644 --- a/src/lib/util/minionStatus.ts +++ b/src/lib/util/minionStatus.ts @@ -519,7 +519,7 @@ export function minionStatus(user: MUser) { case 'Raids': { const data = currentTask as RaidsOptions; - return `${name} is currently doing the Chamber's of Xeric${ + return `${name} is currently doing the Chambers of Xeric${ data.challengeMode ? ' in Challenge Mode' : '' }, ${ data.users.length === 1 ? 'as a solo.' : `with a team of ${data.users.length} minions.` diff --git a/tests/unit/snapshots/clsnapshots.test.ts.snap b/tests/unit/snapshots/clsnapshots.test.ts.snap index 9da493f6c6..f439565780 100644 --- a/tests/unit/snapshots/clsnapshots.test.ts.snap +++ b/tests/unit/snapshots/clsnapshots.test.ts.snap @@ -16,7 +16,7 @@ Camdozaal (10) Capes (143) Castle Wars (39) Cerberus (7) -Chamber's of Xeric (23) +Chambers of Xeric (23) Champion's Challenge (11) Chaos Druids (3) Chaos Elemental (3) From af74d66aa57227613949b3e8aab0a44a3a7c526e Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:38:34 +1100 Subject: [PATCH 24/28] Make ge price history use weekly averages --- src/mahoji/commands/ge.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/mahoji/commands/ge.ts b/src/mahoji/commands/ge.ts index 33cea0f5ed..d4ec891f94 100644 --- a/src/mahoji/commands/ge.ts +++ b/src/mahoji/commands/ge.ts @@ -392,11 +392,11 @@ The next buy limit reset is at: ${GrandExchange.getInterval().nextResetStr}, it return patronMsg(PerkTier.Four); } let result = await prisma.$queryRawUnsafe< - { quantity_bought: number; price_per_item_before_tax: number; created_at: Date }[] + { total_quantity_bought: number; average_price_per_item_before_tax: number; week: Date }[] >(`SELECT - sellTransactions.created_at, - sellTransactions.price_per_item_before_tax, - sellTransactions.quantity_bought + DATE_TRUNC('week', sellTransactions.created_at) AS week, + AVG(sellTransactions.price_per_item_before_tax) AS average_price_per_item_before_tax, + SUM(sellTransactions.quantity_bought) AS total_quantity_bought FROM ge_listing INNER JOIN @@ -407,15 +407,18 @@ AND ge_listing.cancelled_at IS NULL AND ge_listing.fulfilled_at IS NOT NULL +GROUP BY + week ORDER BY - sellTransactions.created_at ASC;`); - if (result.length < 2) return 'No price history found for that item.'; - if (result[0].price_per_item_before_tax <= 1_000_000) { - result = result.filter(i => i.quantity_bought > 1); + week ASC; +`); + if (result.length < 1) return 'No price history found for that item.'; + if (result[0].average_price_per_item_before_tax <= 1_000_000) { + result = result.filter(i => i.total_quantity_bought > 1); } const buffer = await lineChart( `Price History for ${item.name}`, - result.map(i => [new Date(i.created_at).toDateString(), i.price_per_item_before_tax]), + result.map(i => [new Date(i.week).toDateString(), i.average_price_per_item_before_tax]), val => val.toString(), val => val, false From c0595e800f8733d3d1d24b0ba27e1d8ff693adaf Mon Sep 17 00:00:00 2001 From: GC <30398469+gc@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:49:16 +1100 Subject: [PATCH 25/28] Cleanup ci tests (#5713) --- .github/workflows/codequality.yml | 69 ------------------- .../{inttests.yml => integration_tests.yml} | 0 .../workflows/{test.yml => unit_tests.yml} | 9 +++ 3 files changed, 9 insertions(+), 69 deletions(-) delete mode 100644 .github/workflows/codequality.yml rename .github/workflows/{inttests.yml => integration_tests.yml} (100%) rename .github/workflows/{test.yml => unit_tests.yml} (80%) diff --git a/.github/workflows/codequality.yml b/.github/workflows/codequality.yml deleted file mode 100644 index dbc51150ef..0000000000 --- a/.github/workflows/codequality.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Code Quality - -on: - push: - branches: - - master - - bso - pull_request: - -jobs: - ESLint: - name: ESLint - runs-on: ubuntu-latest - steps: - - name: Checkout Project - uses: actions/checkout@v3 - - name: Use Node.js 18.12.0 - uses: actions/setup-node@v3 - with: - node-version: 18.12.0 - cache: yarn - - name: Restore CI Cache - uses: actions/cache@v3 - with: - path: node_modules - key: ${{ runner.os }}-18-${{ hashFiles('**/yarn.lock') }} - - name: Install Dependencies - run: yarn --frozen-lockfile - - name: Generate Prisma Client - run: yarn gen - - name: Run ESLint on changed files - uses: tj-actions/eslint-changed-files@v21 - with: - skip_annotations: true - config_path: ".eslintrc.json" - ignore_path: ".eslintignore" - file_extensions: | - **/*.ts - **/*.tsx - - Typescript: - name: Typescript - runs-on: ubuntu-latest - steps: - - name: Checkout Project - uses: actions/checkout@v3 - - name: Use Node.js 18.12.0 - uses: actions/setup-node@v3 - with: - node-version: 18.12.0 - cache: yarn - - name: Restore CI Cache - uses: actions/cache@v3 - with: - path: node_modules - key: ${{ runner.os }}-18-${{ hashFiles('**/yarn.lock') }} - - name: Install Dependencies - run: yarn --frozen-lockfile - - name: Copy Configuration - run: | - pushd src && - cp config.example.ts config.ts && - popd - - name: Copy env - run: cp .env.example .env - - name: Generate Prisma Client - run: yarn gen - - name: Build code - run: yarn build diff --git a/.github/workflows/inttests.yml b/.github/workflows/integration_tests.yml similarity index 100% rename from .github/workflows/inttests.yml rename to .github/workflows/integration_tests.yml diff --git a/.github/workflows/test.yml b/.github/workflows/unit_tests.yml similarity index 80% rename from .github/workflows/test.yml rename to .github/workflows/unit_tests.yml index 82a0ad2867..bdfa62cdbc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/unit_tests.yml @@ -40,6 +40,15 @@ jobs: run: cp .env.example .env - name: Generate Prisma Client run: yarn gen + - name: Run ESLint on changed files + uses: tj-actions/eslint-changed-files@v21 + with: + skip_annotations: true + config_path: ".eslintrc.json" + ignore_path: ".eslintignore" + file_extensions: | + **/*.ts + **/*.tsx - name: Build run: yarn build - name: Test From 9f0736808e245e923c44cc85ad3786356461adb6 Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Thu, 22 Feb 2024 06:51:40 +0000 Subject: [PATCH 26/28] Fix Zahur/Wesley Costs (#5706) Fix the check on wesley/zahur to pick the right cost if the users selects true to both options, it also returns the correct name. Closes #5698 --- src/mahoji/commands/mix.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mahoji/commands/mix.ts b/src/mahoji/commands/mix.ts index 14f553e2c6..900e8659a9 100644 --- a/src/mahoji/commands/mix.ts +++ b/src/mahoji/commands/mix.ts @@ -92,8 +92,10 @@ export const mixCommand: OSBMahojiCommand = { if ((zahur && mixableZahur) || (wesley && mixableWesley)) { timeToMixSingleItem = 0.000_001; - requiredItems.add('Coins', wesley ? 50 : 200); - cost = `decided to pay ${wesley ? 'Wesley 50' : 'Zahur 200'} gp for each item so they don't have to go`; + requiredItems.add('Coins', mixableWesley ? 50 : 200); + cost = `decided to pay ${ + mixableWesley ? 'Wesley 50' : 'Zahur 200' + } gp for each item so they don't have to go.`; } const maxTripLength = calcMaxTripLength(user, 'Herblore'); From cb94307932cfbf05a99856ac27c69114d6851012 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:52:33 +1100 Subject: [PATCH 27/28] Cant repeat stronghold --- src/lib/util/repeatStoredTrip.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/util/repeatStoredTrip.ts b/src/lib/util/repeatStoredTrip.ts index ceb8b12281..a41bbb3975 100644 --- a/src/lib/util/repeatStoredTrip.ts +++ b/src/lib/util/repeatStoredTrip.ts @@ -76,7 +76,8 @@ export const taskCanBeRepeated = (activity: Activity) => { activity_type_enum.BlastFurnace, activity_type_enum.Easter, activity_type_enum.TokkulShop, - activity_type_enum.Birdhouse + activity_type_enum.Birdhouse, + activity_type_enum.StrongholdOfSecurity ] as activity_type_enum[] ).includes(activity.type); }; From 982591e35bbbff81f59b2f25317256b7a4c82f6f Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:08:04 +1100 Subject: [PATCH 28/28] Add view_all_items rp action --- src/mahoji/commands/rp.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/mahoji/commands/rp.ts b/src/mahoji/commands/rp.ts index c7d5358205..9b653f75c6 100644 --- a/src/mahoji/commands/rp.ts +++ b/src/mahoji/commands/rp.ts @@ -21,7 +21,7 @@ import { allPerkBitfields } from '../../lib/perkTiers'; import { prisma } from '../../lib/settings/prisma'; import { TeamLoot } from '../../lib/simulation/TeamLoot'; import { ItemBank } from '../../lib/types'; -import { dateFm, formatDuration } from '../../lib/util'; +import { dateFm, formatDuration, returnStringOrFile } from '../../lib/util'; import getOSItem from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction } from '../../lib/util/interactionReply'; @@ -72,6 +72,12 @@ export const rpCommand: OSBMahojiCommand = { name: 'patreon_reset', description: 'Reset all patreon data.', options: [] + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'view_all_items', + description: 'View all item IDs present in banks/cls.', + options: [] } ] }, @@ -302,6 +308,7 @@ export const rpCommand: OSBMahojiCommand = { action?: { validate_ge?: {}; patreon_reset?: {}; + view_all_items?: {}; }; player?: { viewbank?: { user: MahojiUserOption; json?: boolean }; @@ -349,6 +356,18 @@ export const rpCommand: OSBMahojiCommand = { return 'Something was invalid. Check logs!'; } + if (options.action?.view_all_items) { + const result = await prisma.$queryRawUnsafe< + { item_id: number }[] + >(`SELECT DISTINCT json_object_keys(bank)::int AS item_id +FROM users +UNION +SELECT DISTINCT jsonb_object_keys("collectionLogBank")::int AS item_id +FROM users +ORDER BY item_id ASC;`); + return returnStringOrFile(`[${result.map(i => i.item_id).join(',')}]`); + } + if (options.action?.patreon_reset) { const bitfieldsToRemove = [ BitField.IsPatronTier1,