diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 165927f7c5..5442864076 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -20,4 +20,4 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Run Integration Tests - run: docker-compose up --build --abort-on-container-exit + run: docker compose up --build --abort-on-container-exit --remove-orphans && docker compose down --volumes --remove-orphans diff --git a/SETUP.md b/SETUP.md index 87c7f5d7a9..ee08f7bdf0 100644 --- a/SETUP.md +++ b/SETUP.md @@ -15,7 +15,7 @@ This assumes you are using VSCode as your IDE. If you have errors or issues, you 2. Install [Postgres 16](https://www.postgresql.org/download/) and PGAdmin4 for interacting with postgres (optional, but helpful) 3. Install Yarn using: `npm i -g yarn` 4. Clone the repo: `git clone https://github.com/oldschoolgg/oldschoolbot.git` -5. Run `corepack enable` and `yarn` in the root of the repo. +5. Run the following commands in the root of the repo: `corepack enable`, `yarn`, `npx prisma db push` and `npx prisma db push --schema ./prisma/robochimp.prisma` ### Configuration diff --git a/package.json b/package.json index 2cd90d2506..a11ef3d90e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "scripts": { - "watch": "nodemon -e ts -w src --exec 'yarn buildandrun'", + "watch": "nodemon --delay 1ms -e ts -w src --exec 'yarn buildandrun'", "build": "tsx ./src/scripts/build.ts", "fix": "tsx ./src/scripts/troubleshooter.ts", "start": "yarn build && node --enable-source-maps dist/", @@ -14,7 +14,7 @@ "test": "concurrently --raw --kill-others-on-fail \"tsc -p src && yarn test:circular\" \"yarn test:lint\" \"yarn test:unit\" \"tsc -p tests/integration --noEmit\" \"tsc -p tests/unit --noEmit\"", "test:lint": "biome check --diagnostic-level=error", "test:unit": "vitest run --coverage --config vitest.unit.config.mts", - "test:docker": "docker-compose up --build --abort-on-container-exit --remove-orphans && docker-compose down --volumes --remove-orphans", + "test:docker": "docker compose up --build --abort-on-container-exit --remove-orphans && docker compose down --volumes --remove-orphans", "test:watch": "vitest --config vitest.unit.config.mts --coverage", "buildandrun": "yarn build:esbuild && node --enable-source-maps dist", "build:esbuild": "concurrently --raw \"yarn build:main\" \"yarn build:workers\"", @@ -25,17 +25,19 @@ }, "dependencies": { "@napi-rs/canvas": "^0.1.53", - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#3550582efbdf04929f0a2c5114ed069a61551807", + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed", "@prisma/client": "^5.17.0", "@sapphire/ratelimits": "^2.4.9", "@sapphire/snowflake": "^3.5.3", "@sapphire/time-utilities": "^1.6.0", + "@sapphire/timer-manager": "^1.0.2", "@sentry/node": "^8.15.0", "ascii-table3": "^0.9.0", "bufferutil": "^4.0.8", "discord.js": "^14.15.3", "dotenv": "^16.4.5", "e": "0.2.33", + "exit-hook": "^4.0.0", "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21", "lru-cache": "^10.3.0", @@ -46,6 +48,7 @@ "p-queue": "^6.6.2", "piscina": "^4.6.1", "random-js": "^2.1.0", + "remeda": "^2.7.0", "simple-statistics": "^7.8.3", "sonic-boom": "^4.0.1", "zlib-sync": "^0.1.9", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e25b832224..0c894e7942 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -404,7 +404,6 @@ model User { store_bitfield Int[] @default([]) - // Migrate farmingPatches_herb Json? @map("farmingPatches.herb") @db.Json farmingPatches_fruit_tree Json? @map("farmingPatches.fruit tree") @db.Json farmingPatches_tree Json? @map("farmingPatches.tree") @db.Json @@ -436,6 +435,8 @@ model User { gift_boxes_owned GiftBox[] @relation("gift_boxes_owned") gift_boxes_created GiftBox[] @relation("gift_boxes_created") + cl_array Int[] @default([]) + @@index([id, last_command_date]) @@map("users") } @@ -744,7 +745,7 @@ model UserStats { creature_scores Json @default("{}") monster_scores Json @default("{}") laps_scores Json @default("{}") - sacrificed_bank Json @default("{}") + sacrificed_bank Json @default("{}") @db.JsonB openable_scores Json @default("{}") gp_luckypick BigInt @default(0) diff --git a/src/index.ts b/src/index.ts index a2d90c39d4..e9cce8683f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,13 +18,13 @@ import { onMessage } from './lib/events'; import { modalInteractionHook } from './lib/modals'; import { preStartup } from './lib/preStartup'; import { OldSchoolBotClient } from './lib/structures/OldSchoolBotClient'; -import { runTimedLoggedFn } from './lib/util'; import { CACHED_ACTIVE_USER_IDS } from './lib/util/cachedUserIDs'; import { interactionHook } from './lib/util/globalInteractions'; import { handleInteractionError, interactionReply } from './lib/util/interactionReply'; import { logError } from './lib/util/logError'; import { allCommands } from './mahoji/commands/allCommands'; import { onStartup } from './mahoji/lib/events'; +import { exitCleanup } from './mahoji/lib/exitHandler'; import { postCommand } from './mahoji/lib/postCommand'; import { preCommand } from './mahoji/lib/preCommand'; import { convertMahojiCommandToAbstractCommand } from './mahoji/lib/util'; @@ -194,9 +194,16 @@ client.on('shardError', err => debugLog('Shard Error', { error: err.message })); client.once('ready', () => onStartup()); async function main() { + await Promise.all([ + preStartup(), + import('exit-hook').then(({ asyncExitHook }) => + asyncExitHook(exitCleanup, { + wait: 2000 + }) + ) + ]); if (process.env.TEST) return; - await preStartup(); - await runTimedLoggedFn('Log In', () => client.login(globalConfig.botToken)); + await client.login(globalConfig.botToken); console.log(`Logged in as ${globalClient.user.username}`); } diff --git a/src/lib/MUser.ts b/src/lib/MUser.ts index 3c4bd10f0e..c9b47bac4f 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -137,7 +137,7 @@ export class MUserClass { this.bitfield = this.user.bitfield as readonly BitField[]; } - countSkillsAtleast99() { + countSkillsAtLeast99() { return Object.values(this.skillsAsLevels).filter(lvl => lvl >= 99).length; } diff --git a/src/lib/Task.ts b/src/lib/Task.ts index c243efe2ee..74492fa491 100644 --- a/src/lib/Task.ts +++ b/src/lib/Task.ts @@ -96,7 +96,6 @@ import { modifyBusyCounter } from './busyCounterCache'; import { minionActivityCache } from './constants'; import { convertStoredActivityToFlatActivity } from './settings/prisma'; import { activitySync, minionActivityCacheDelete } from './settings/settings'; -import { logWrapFn } from './util'; import { logError } from './util/logError'; const tasks: MinionTask[] = [ @@ -219,13 +218,13 @@ export async function processPendingActivities() { } } -export const syncActivityCache = logWrapFn('syncActivityCache', async () => { +export const syncActivityCache = async () => { const tasks = await prisma.activity.findMany({ where: { completed: false } }); minionActivityCache.clear(); for (const task of tasks) { activitySync(task); } -}); +}; const ActivityTaskOptionsSchema = z.object({ userID: z.string(), diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 1d3229abf7..ee966f8cb9 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -45,7 +45,7 @@ export async function analyticsTick() { ).map((result: any) => Number.parseInt(result[0].count)) as number[]; const taskCounts = await calculateMinionTaskCounts(); - const currentClientSettings = await await prisma.clientStorage.findFirst({ + const currentClientSettings = await prisma.clientStorage.upsert({ where: { id: globalConfig.clientID }, @@ -64,9 +64,12 @@ export async function analyticsTick() { gp_slots: true, gp_tax_balance: true, economyStats_dailiesAmount: true - } + }, + create: { + id: globalConfig.clientID + }, + update: {} }); - if (!currentClientSettings) throw new Error('No client settings found'); await prisma.analytic.create({ data: { guildsCount: globalClient.guilds.cache.size, diff --git a/src/lib/blacklists.ts b/src/lib/blacklists.ts index 7ae966f68d..278e74d6af 100644 --- a/src/lib/blacklists.ts +++ b/src/lib/blacklists.ts @@ -1,6 +1,6 @@ import { Time } from 'e'; -import { production } from '../config'; +import { TimerManager } from '@sapphire/timer-manager'; export const BLACKLISTED_USERS = new Set(); export const BLACKLISTED_GUILDS = new Set(); @@ -15,6 +15,4 @@ export async function syncBlacklists() { } } -if (production) { - setInterval(syncBlacklists, Time.Minute * 10); -} +TimerManager.setInterval(syncBlacklists, Time.Minute * 10); diff --git a/src/lib/colosseum.ts b/src/lib/colosseum.ts index 1ddd0229ca..2a37ae359e 100644 --- a/src/lib/colosseum.ts +++ b/src/lib/colosseum.ts @@ -366,6 +366,9 @@ interface ColosseumResult { realDuration: number; totalDeathChance: number; deathChances: number[]; + scytheCharges: number; + venatorBowCharges: number; + bloodFuryCharges: number; } export const startColosseumRun = (options: { @@ -377,6 +380,9 @@ export const startColosseumRun = (options: { hasClaws: boolean; hasSGS: boolean; hasTorture: boolean; + scytheCharges: number; + venatorBowCharges: number; + bloodFuryCharges: number; }): ColosseumResult => { const waveTwelveKC = options.kcBank.amount(12); @@ -406,6 +412,10 @@ export const startColosseumRun = (options: { let realDuration = 0; let maxGlory = 0; + // Calculate charges used + const scytheCharges = 300; + const calculateVenCharges = () => 50; + for (const wave of colosseumWaves) { realDuration += waveDuration; const kcForThisWave = options.kcBank.amount(wave.waveNumber); @@ -422,7 +432,10 @@ export const startColosseumRun = (options: { fakeDuration, realDuration, totalDeathChance: combinedChance(deathChances), - deathChances + deathChances, + scytheCharges: options.hasScythe ? scytheCharges : 0, + venatorBowCharges: options.hasVenBow ? calculateVenCharges() : 0, + bloodFuryCharges: options.hasBF ? scytheCharges * 3 : 0 }; } addedWaveKCBank.add(wave.waveNumber); @@ -436,7 +449,11 @@ export const startColosseumRun = (options: { fakeDuration, realDuration, totalDeathChance: combinedChance(deathChances), - deathChances + deathChances, + + scytheCharges: options.hasScythe ? scytheCharges : 0, + venatorBowCharges: options.hasVenBow ? calculateVenCharges() : 0, + bloodFuryCharges: options.hasBF ? scytheCharges * 3 : 0 }; } } @@ -532,6 +549,9 @@ export async function colosseumCommand(user: MUser, channelID: string) { const hasClaws = user.hasEquippedOrInBank('Dragon claws'); const hasSGS = user.hasEquippedOrInBank('Saradomin godsword'); const hasTorture = !hasBF && user.gear.melee.hasEquipped('Amulet of torture'); + const scytheCharges = 300; + const bloodFuryCharges = scytheCharges * 3; + const venatorBowCharges = calculateVenCharges(); const res = startColosseumRun({ kcBank: new ColosseumWaveBank((await user.fetchStats({ colo_kc_bank: true })).colo_kc_bank as ItemBank), @@ -541,7 +561,10 @@ export async function colosseumCommand(user: MUser, channelID: string) { hasBF, hasClaws, hasSGS, - hasTorture + hasTorture, + scytheCharges, + venatorBowCharges, + bloodFuryCharges }); const minutes = res.realDuration / Time.Minute; @@ -556,7 +579,6 @@ export async function colosseumCommand(user: MUser, channelID: string) { return 'You need to have a Ranging potion(4) or Bastion potion(4) in your bank.'; } - const scytheCharges = 300; if (hasScythe) { messages.push('10% boost for Scythe'); chargeBank.add('scythe_of_vitur_charges', scytheCharges); @@ -578,7 +600,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { cost.add('Dragon arrow', 50); } else { messages.push( - 'Missed 7% Venator bow boost. If you have one, charge it and keep it in your bank. You also need atleast 50 dragon arrows equipped.' + 'Missed 7% Venator bow boost. If you have one, charge it and keep it in your bank. You also need at least 50 dragon arrows equipped.' ); } @@ -595,7 +617,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { } if (user.gear.melee.hasEquipped('Amulet of blood fury')) { - chargeBank.add('blood_fury_charges', scytheCharges * 3); + chargeBank.add('blood_fury_charges', bloodFuryCharges); messages.push('-5% death chance for blood fury'); } else { messages.push('Missed -5% death chance for blood fury. If you have one, add charges and equip it to melee.'); @@ -652,7 +674,10 @@ export async function colosseumCommand(user: MUser, channelID: string) { fakeDuration: res.fakeDuration, maxGlory: res.maxGlory, diedAt: res.diedAt ?? undefined, - loot: res.loot?.bank + loot: res.loot?.bank, + scytheCharges: res.scytheCharges, + venatorBowCharges: res.venatorBowCharges, + bloodFuryCharges: res.bloodFuryCharges }); return `${user.minionName} is now attempting the Colosseum. They will finish in around ${formatDuration( diff --git a/src/lib/combat_achievements/combatAchievements.ts b/src/lib/combat_achievements/combatAchievements.ts index 5545b07c0d..5aef785fb0 100644 --- a/src/lib/combat_achievements/combatAchievements.ts +++ b/src/lib/combat_achievements/combatAchievements.ts @@ -162,13 +162,13 @@ const indexesWithRng = entries.flatMap(i => i[1].tasks.filter(t => 'rng' in t)); export const combatAchievementTripEffect = async ({ data, messages, user }: Parameters[0]) => { const dataCopy = deepClone(data); if (dataCopy.type === 'Inferno' && !dataCopy.diedPreZuk && !dataCopy.diedZuk) { - (dataCopy as any).quantity = 1; + (dataCopy as any).q = 1; } if (dataCopy.type === 'Colosseum') { - (dataCopy as any).quantity = 1; + (dataCopy as any).q = 1; } - if (!('quantity' in dataCopy)) return; - let quantity = Number(dataCopy.quantity); + if (!('q' in dataCopy)) return; + let quantity = Number(dataCopy.q); if (Number.isNaN(quantity)) return; if (data.type === 'TombsOfAmascut') { diff --git a/src/lib/combat_achievements/hard.ts b/src/lib/combat_achievements/hard.ts index 82f33c3077..1522239efb 100644 --- a/src/lib/combat_achievements/hard.ts +++ b/src/lib/combat_achievements/hard.ts @@ -597,7 +597,7 @@ export const hardCombatAchievements: CombatAchievement[] = [ monster: 'Tempoross', desc: 'Subdue Tempoross, getting rewarded with 10 reward permits from a single Tempoross fight.', rng: { - chancePerKill: 30, + chancePerKill: 5, hasChance: data => data.type === 'Tempoross' } }, diff --git a/src/lib/combat_achievements/medium.ts b/src/lib/combat_achievements/medium.ts index 420f3c1b43..30d8255701 100644 --- a/src/lib/combat_achievements/medium.ts +++ b/src/lib/combat_achievements/medium.ts @@ -431,7 +431,7 @@ export const mediumCombatAchievements: CombatAchievement[] = [ monster: 'Skotizo', desc: 'Kill Skotizo with no altars active.', rng: { - chancePerKill: 15, + chancePerKill: 5, hasChance: isCertainMonsterTrip(Monsters.Skotizo.id) } }, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index df22a874b6..cb7b989f77 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,5 +1,6 @@ import { execSync } from 'node:child_process'; import path from 'node:path'; +import { isMainThread } from 'node:worker_threads'; import type { Image } from '@napi-rs/canvas'; import { PerkTier, SimpleTable, StoreBitfield, dateFm } from '@oldschoolgg/toolkit'; import type { CommandOptions } from '@oldschoolgg/toolkit'; @@ -197,7 +198,6 @@ export enum BitField { IsPatronTier4 = 5, IsPatronTier5 = 6, isModerator = 7, - isContributor = 8, BypassAgeRestriction = 9, HasHosidiusWallkit = 10, HasPermanentEventBackgrounds = 11, @@ -208,7 +208,6 @@ export enum BitField { HasDexScroll = 16, HasArcaneScroll = 17, HasTornPrayerScroll = 18, - IsWikiContributor = 19, HasSlepeyTablet = 20, IsPatronTier6 = 21, DisableBirdhouseRunButton = 22, @@ -244,9 +243,7 @@ interface BitFieldData { } export const BitFieldData: Record = { - [BitField.IsWikiContributor]: { name: 'Wiki Contributor', protected: true, userConfigurable: false }, [BitField.isModerator]: { name: 'Moderator', protected: true, userConfigurable: false }, - [BitField.isContributor]: { name: 'Contributor', protected: true, userConfigurable: false }, [BitField.HasPermanentTierOne]: { name: 'Permanent Tier 1', protected: false, userConfigurable: false }, [BitField.IsPatronTier1]: { name: 'Tier 1 Patron', protected: false, userConfigurable: false }, @@ -347,7 +344,9 @@ export const BadgesEnum = { TopSkiller: 9, TopCollector: 10, TopMinigame: 11, - SotWTrophy: 12 + SotWTrophy: 12, + Slayer: 13, + TopGiveawayer: 14 } as const; export const badges: { [key: number]: string } = { @@ -363,7 +362,9 @@ export const badges: { [key: number]: string } = { [BadgesEnum.TopSkiller]: Emoji.Skiller, [BadgesEnum.TopCollector]: Emoji.CollectionLog, [BadgesEnum.TopMinigame]: Emoji.MinigameIcon, - [BadgesEnum.SotWTrophy]: Emoji.SOTWTrophy + [BadgesEnum.SotWTrophy]: Emoji.SOTWTrophy, + [BadgesEnum.Slayer]: Emoji.Slayer, + [BadgesEnum.TopGiveawayer]: Emoji.SantaHat }; export const MAX_XP = 200_000_000; @@ -627,7 +628,7 @@ export const winterTodtPointsTable = new SimpleTable() .add(780) .add(850); -if (!process.env.TEST) { +if (!process.env.TEST && isMainThread) { console.log( `Starting... Git[${gitHash}] ClientID[${globalConfig.clientID}] Production[${globalConfig.isProduction}]` ); diff --git a/src/lib/crons.ts b/src/lib/crons.ts index a5742fef2d..ae104c06c5 100644 --- a/src/lib/crons.ts +++ b/src/lib/crons.ts @@ -37,7 +37,6 @@ GROUP BY item_id;`); * Delete all voice channels */ schedule('0 0 */1 * *', async () => { - debugLog('Cache cleanup cronjob starting'); cacheCleanup(); }); diff --git a/src/lib/data/Collections.ts b/src/lib/data/Collections.ts index 84726fe90d..7e3a4c1f29 100644 --- a/src/lib/data/Collections.ts +++ b/src/lib/data/Collections.ts @@ -328,7 +328,8 @@ export const allCollectionLogs: ICollection = { 'Sunfire fanatic helm', 'Echo crystal', 'Tonalztics of ralos (uncharged)', - 'Sunfire splinters' + 'Sunfire splinters', + 'Uncut onyx' ]), fmtProg: ({ minigames }) => `${minigames.colosseum} KC` }, diff --git a/src/lib/data/buyables/skillCapeBuyables.ts b/src/lib/data/buyables/skillCapeBuyables.ts index d6061f7a9f..fddc4fd69c 100644 --- a/src/lib/data/buyables/skillCapeBuyables.ts +++ b/src/lib/data/buyables/skillCapeBuyables.ts @@ -12,7 +12,7 @@ for (const skillcape of Skillcapes) { outputItems: (user: MUser) => { const output = new Bank().add(skillcape.hood); - if (user.countSkillsAtleast99() > 1) { + if (user.countSkillsAtLeast99() > 1) { output.add(skillcape.trimmed); } else { output.add(skillcape.untrimmed); diff --git a/src/lib/data/cox.ts b/src/lib/data/cox.ts index 05a7df1fc7..912f2ecf42 100644 --- a/src/lib/data/cox.ts +++ b/src/lib/data/cox.ts @@ -240,11 +240,11 @@ export const minimumCoxSuppliesNeeded = new Bank({ export async function checkCoxTeam(users: MUser[], cm: boolean, quantity = 1): Promise { const hasHerbalist = users.some(u => u.skillLevel(SkillsEnum.Herblore) >= 78); if (!hasHerbalist) { - return 'nobody with atleast level 78 Herblore'; + return 'nobody with at least level 78 Herblore'; } const hasFarmer = users.some(u => u.skillLevel(SkillsEnum.Farming) >= 55); if (!hasFarmer) { - return 'nobody with atleast level 55 Farming'; + return 'nobody with at least level 55 Farming'; } const suppliesNeeded = minimumCoxSuppliesNeeded.clone().multiply(quantity); const userWithoutSupplies = users.find(u => !u.bank.has(suppliesNeeded)); diff --git a/src/lib/data/itemAliases.ts b/src/lib/data/itemAliases.ts index ec44fda255..557bd0b85f 100644 --- a/src/lib/data/itemAliases.ts +++ b/src/lib/data/itemAliases.ts @@ -178,12 +178,15 @@ setItemAlias(2993, 'Chompy bird hat (dragon archer)'); setItemAlias(2994, 'Chompy bird hat (expert ogre dragon archer)'); setItemAlias(2995, 'Chompy bird hat (expert dragon archer)'); -// Item aliases +// Achievement diary lamps setItemAlias(11_137, 'Antique lamp 1'); setItemAlias(11_139, 'Antique lamp 2'); setItemAlias(11_141, 'Antique lamp 3'); setItemAlias(11_185, 'Antique lamp 4'); +// Defender of varrock quest lamp +setItemAlias(28_820, 'Antique lamp (defender of varrock)'); + // Dragonfire shields setItemAlias(11_284, 'Uncharged dragonfire shield'); setItemAlias(11_283, 'Dragonfire shield'); diff --git a/src/lib/degradeableItems.ts b/src/lib/degradeableItems.ts index 47807b39d8..42a0d37496 100644 --- a/src/lib/degradeableItems.ts +++ b/src/lib/degradeableItems.ts @@ -61,6 +61,13 @@ interface DegradeableItemPVMBoost { boost: number; } +interface RefundResult { + item: Item; + refundedCharges: number; + totalCharges: number; + userMessage: string; +} + export const degradeableItems: DegradeableItem[] = [ { item: getOSItem('Abyssal tentacle'), @@ -389,10 +396,8 @@ 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.${ - pennyReduction > 0 ? ` Your Ghommal's lucky penny saved ${pennyReduction} charges` : '' + userMessage: `Your ${item.name} degraded by ${chargesToDegrade} charges, and now has ${chargesAfter} remaining${ + pennyReduction > 0 ? `. Your Ghommal's lucky penny saved ${pennyReduction} charges` : '' }` }; } @@ -426,3 +431,38 @@ export async function degradeChargeBank(user: MUser, chargeBank: ChargeBank) { return results; } + +export async function refundChargeBank(user: MUser, chargeBank: ChargeBank): Promise { + const results: RefundResult[] = []; + + for (const [key, chargesToRefund] of chargeBank.entries()) { + const degItem = degradeableItems.find(i => i.settingsKey === key); + if (!degItem) { + throw new Error(`Invalid degradeable item settings key: ${key}`); + } + + const currentCharges = user.user[degItem.settingsKey]; + const newCharges = currentCharges + chargesToRefund; + + // Prepare result message + const userMessage = `Refunded ${chargesToRefund} charges for ${degItem.item.name}.`; + + // Create result object + const result: RefundResult = { + item: degItem.item, + refundedCharges: chargesToRefund, + totalCharges: newCharges, + userMessage + }; + + // Push result to results array + results.push(result); + + // Update user + await user.update({ + [degItem.settingsKey]: newCharges + }); + } + + return results; +} diff --git a/src/lib/diaries.ts b/src/lib/diaries.ts index d74961b72a..861361f43d 100644 --- a/src/lib/diaries.ts +++ b/src/lib/diaries.ts @@ -468,13 +468,7 @@ export const FaladorDiary: Diary = { woodcutting: 71 }, qp: 32, - collectionLogReqs: resolveItems([ - 'Mind rune', - 'Prospector jacket', - 'Prospector helmet', - 'Prospector legs', - 'Prospector boots' - ]), + collectionLogReqs: resolveItems(['Mind rune', 'Prospector helmet']), monsterScores: { 'Skeletal Wyvern': 1, 'Blue Dragon': 1 diff --git a/src/lib/events.ts b/src/lib/events.ts index 8b0ba8c14b..7a383eeb4e 100644 --- a/src/lib/events.ts +++ b/src/lib/events.ts @@ -43,6 +43,8 @@ const rareRolesSrc: [string, number, string][] = [ const userCache = new LRUCache({ max: 1000 }); function rareRoles(msg: Message) { + if (!globalConfig.isProduction) return; + if (!msg.guild || msg.guild.id !== SupportServer) { return; } diff --git a/src/lib/handleNewCLItems.ts b/src/lib/handleNewCLItems.ts index c57afd9041..d5e84942fa 100644 --- a/src/lib/handleNewCLItems.ts +++ b/src/lib/handleNewCLItems.ts @@ -9,6 +9,7 @@ import { allCLItems, allCollectionLogsFlat, calcCLDetails } from './data/Collect import { calculateMastery } from './mastery'; import { calculateOwnCLRanking, roboChimpSyncData } from './roboChimp'; +import { RawSQL } from './rawSql'; import { MUserStats } from './structures/MUserStats'; import { fetchCLLeaderboard } from './util/clLeaderboard'; import { insertUserEvent } from './util/userEvents'; @@ -49,6 +50,10 @@ export async function handleNewCLItems({ await prisma.historicalData.create({ data: await createHistoricalData(user) }); } + if (didGetNewCLItem) { + await prisma.$queryRawUnsafe(RawSQL.updateCLArray(user.id)); + } + if (!didGetNewCLItem) return; const previousCLDetails = calcCLDetails(previousCL); @@ -101,15 +106,14 @@ export async function handleNewCLItems({ })}!` : ''; - const nthUser = ( - await fetchCLLeaderboard({ - ironmenOnly: false, - items: finishedCL.items, - resultLimit: 100_000, - method: 'raw_cl', - userEvents: null - }) - ).filter(u => u.qty === finishedCL.items.length).length; + const leaderboardUsers = await fetchCLLeaderboard({ + ironmenOnly: false, + items: finishedCL.items, + resultLimit: 100_000, + clName: finishedCL.name + }); + + const nthUser = leaderboardUsers.users.filter(u => u.qty === finishedCL.items.length).length; const placeStr = nthUser > 100 ? '' : ` They are the ${formatOrdinal(nthUser)} user to finish this CL.`; diff --git a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts index d6c0feb8b6..23e6c5ff52 100644 --- a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts +++ b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts @@ -243,7 +243,6 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 12, attackStyleToUse: GearStat.AttackRanged, attackStylesUsed: [GearStat.AttackMagic], - canCannon: true, pkActivityRating: 4, pkBaseDeathChance: 6, revsWeaponBoost: true diff --git a/src/lib/minions/data/quests.ts b/src/lib/minions/data/quests.ts index 70451b6ac9..91a92ec0cb 100644 --- a/src/lib/minions/data/quests.ts +++ b/src/lib/minions/data/quests.ts @@ -84,7 +84,7 @@ export const quests: Quest[] = [ }, combatLevelReq: 50, qpReq: 10, - rewards: new Bank().add(28_587).add(28_587).add(28_588).add(28_589).add(28_590).freeze(), + rewards: new Bank().add(28_587).add(28_588).add(28_589).add(28_590).freeze(), calcTime: (user: MUser) => { let duration = Time.Minute * 10; if (user.combatLevel < 90) { @@ -112,8 +112,7 @@ export const quests: Quest[] = [ }, combatLevelReq: 65, qpReq: 20, - // Awaiting item update for the lamp to be added - // rewards: new Bank().add(28_820).freeze(), + rewards: new Bank().add(28_820).freeze(), skillsRewards: { smithing: 15_000, hunter: 15_000 diff --git a/src/lib/minions/functions/index.ts b/src/lib/minions/functions/index.ts index f3bd7b49eb..2464f0acec 100644 --- a/src/lib/minions/functions/index.ts +++ b/src/lib/minions/functions/index.ts @@ -50,7 +50,7 @@ export function resolveAttackStyles( // Automatically use magic if barrage/burst is chosen if ( params.boostMethod && - (params.boostMethod === 'barrage' || params.boostMethod === 'burst') && + (params.boostMethod.includes('barrage') || params.boostMethod.includes('burst')) && !attackStyles.includes(SkillsEnum.Magic) ) { if (attackStyles.includes(SkillsEnum.Defence)) { @@ -63,7 +63,7 @@ export function resolveAttackStyles( } export async function addMonsterXP(user: MUser, params: AddMonsterXpParams) { - const boostMethod = params.burstOrBarrage ? 'barrage' : 'none'; + const boostMethod = params.burstOrBarrage ? ['barrage'] : ['none']; const [, osjsMon, attackStyles] = resolveAttackStyles(user, { monsterID: params.monsterID, diff --git a/src/lib/minions/functions/lmsSimCommand.ts b/src/lib/minions/functions/lmsSimCommand.ts index ef34e9c1bc..ae715ec690 100644 --- a/src/lib/minions/functions/lmsSimCommand.ts +++ b/src/lib/minions/functions/lmsSimCommand.ts @@ -125,7 +125,7 @@ export async function lmsSimCommand(channel: Channel | undefined, names?: string if (filtered.size < 4) { return channel.send( - 'Please specify atleast 4 players for Last Man Standing, like so: `+lms Alex, Kyra, Magna, Rick`, or type `+lms auto` to automatically pick people from the chat.' + 'Please specify at least 4 players for Last Man Standing, like so: `+lms Alex, Kyra, Magna, Rick`, or type `+lms auto` to automatically pick people from the chat.' ); } diff --git a/src/lib/minions/types.ts b/src/lib/minions/types.ts index e5efadbb39..cc81695e13 100644 --- a/src/lib/minions/types.ts +++ b/src/lib/minions/types.ts @@ -177,7 +177,7 @@ export interface AddMonsterXpParams { export interface ResolveAttackStylesParams { monsterID: number | undefined; - boostMethod?: string; + boostMethod?: string[]; } export interface BlowpipeData { diff --git a/src/lib/musicCape.ts b/src/lib/musicCape.ts index ee32e947d1..84bba310e1 100644 --- a/src/lib/musicCape.ts +++ b/src/lib/musicCape.ts @@ -100,7 +100,7 @@ export const musicCapeRequirements = new Requirements() } }) .add({ - name: 'Runecraft all runes atleast once', + name: 'Runecraft all runes at least once', has: ({ uniqueRunesCrafted }) => { const runesToCheck = resolveItems([ 'Mind rune', diff --git a/src/lib/party.ts b/src/lib/party.ts index 7e82e6c19f..f1736a5974 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -1,10 +1,10 @@ import { makeComponents } from '@oldschoolgg/toolkit'; import { UserError } from '@oldschoolgg/toolkit'; +import { TimerManager } from '@sapphire/timer-manager'; import type { TextChannel } from 'discord.js'; import { ButtonBuilder, ButtonStyle, ComponentType, InteractionCollector } from 'discord.js'; import { Time, debounce, noOp } from 'e'; -import { production } from '../config'; import { BLACKLISTED_USERS } from './blacklists'; import { SILENT_ERROR } from './constants'; import type { MakePartyOptions } from './types'; @@ -12,11 +12,9 @@ import { getUsername } from './util'; import { CACHED_ACTIVE_USER_IDS } from './util/cachedUserIDs'; const partyLockCache = new Set(); -if (production) { - setInterval(() => { - partyLockCache.clear(); - }, Time.Minute * 20); -} +TimerManager.setInterval(() => { + partyLockCache.clear(); +}, Time.Minute * 20); const buttons = [ { @@ -226,7 +224,7 @@ export async function setupParty(channel: TextChannel, leaderUser: MUser, option for (const user of usersWhoConfirmed) { partyLockCache.delete(user); } - setTimeout(() => startTrip(), 250); + TimerManager.setTimeout(() => startTrip(), 250); }); }); diff --git a/src/lib/perkTiers.ts b/src/lib/perkTiers.ts index 025b1814af..e25e2db5bb 100644 --- a/src/lib/perkTiers.ts +++ b/src/lib/perkTiers.ts @@ -14,11 +14,7 @@ export const allPerkBitfields: BitField[] = [ ]; export function getUsersPerkTier(user: MUser): PerkTier | 0 { - if ( - [BitField.isContributor, BitField.isModerator, BitField.IsWikiContributor].some(bit => - user.bitfield.includes(bit) - ) - ) { + if ([BitField.isModerator].some(bit => user.bitfield.includes(bit))) { return PerkTier.Four; } diff --git a/src/lib/preStartup.ts b/src/lib/preStartup.ts index 79b604bafb..b47aefc1f0 100644 --- a/src/lib/preStartup.ts +++ b/src/lib/preStartup.ts @@ -1,3 +1,4 @@ +import { noOp } from 'e'; import { syncCustomPrices } from '../mahoji/lib/events'; import { syncActivityCache } from './Task'; import { cacheBadges } from './badges'; @@ -5,22 +6,24 @@ import { syncBlacklists } from './blacklists'; import { GrandExchange } from './grandExchange'; import { cacheGEPrices } from './marketPrices'; import { populateRoboChimpCache } from './perkTier'; +import { RawSQL } from './rawSql'; import { runStartupScripts } from './startupScripts'; -import { runTimedLoggedFn } from './util'; +import { logWrapFn } from './util'; import { syncActiveUserIDs } from './util/cachedUserIDs'; import { syncDisabledCommands } from './util/syncDisabledCommands'; -export async function preStartup() { +export const preStartup = logWrapFn('PreStartup', async () => { await Promise.all([ syncActiveUserIDs(), - runTimedLoggedFn('Sync Activity Cache', syncActivityCache), - runTimedLoggedFn('Startup Scripts', runStartupScripts), - runTimedLoggedFn('Sync Disabled Commands', syncDisabledCommands), - runTimedLoggedFn('Sync Blacklist', syncBlacklists), - runTimedLoggedFn('Syncing prices', syncCustomPrices), - runTimedLoggedFn('Caching badges', cacheBadges), - runTimedLoggedFn('Init Grand Exchange', () => GrandExchange.init()), - runTimedLoggedFn('populateRoboChimpCache', populateRoboChimpCache), - runTimedLoggedFn('Cache G.E Prices', cacheGEPrices) + syncActivityCache(), + runStartupScripts(), + syncDisabledCommands(), + syncBlacklists(), + syncCustomPrices(), + cacheBadges(), + GrandExchange.init(), + populateRoboChimpCache(), + cacheGEPrices(), + prisma.$queryRawUnsafe(RawSQL.updateAllUsersCLArrays()).then(noOp) ]); -} +}); diff --git a/src/lib/rawSql.ts b/src/lib/rawSql.ts new file mode 100644 index 0000000000..45def967e9 --- /dev/null +++ b/src/lib/rawSql.ts @@ -0,0 +1,28 @@ +import { Prisma } from '@prisma/client'; +import { logError } from './util/logError'; + +const u = Prisma.UserScalarFieldEnum; + +export const RawSQL = { + updateAllUsersCLArrays: () => `UPDATE users +SET ${u.cl_array} = ( + SELECT (ARRAY(SELECT jsonb_object_keys("${u.collectionLogBank}")::int)) +) +WHERE last_command_date > now() - INTERVAL '1 week';`, + updateCLArray: (userID: string) => `UPDATE users +SET ${u.cl_array} = ( + SELECT (ARRAY(SELECT jsonb_object_keys("${u.collectionLogBank}")::int)) +) +WHERE ${u.id} = '${userID}';` +}; + +export async function loggedRawPrismaQuery(query: string): Promise { + try { + const result = await prisma.$queryRawUnsafe(query); + return result; + } catch (err) { + logError(err, { query: query.slice(0, 100) }); + } + + return null; +} diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index dc694bb83c..eb76b1d692 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -1,535 +1,453 @@ -import { Prisma } from '@prisma/client'; -import { noOp, notEmpty } from 'e'; +import { noOp, notEmpty, uniqueArr } from 'e'; -import { SupportServer, production } from '../config'; -import { ClueTiers } from '../lib/clues/clueTiers'; -import { Roles } from '../lib/constants'; +import { SupportServer } from '../config'; +import { BadgesEnum, Roles } from '../lib/constants'; import { getCollectionItems } from '../lib/data/Collections'; import { Minigames } from '../lib/settings/minigames'; -import Skills from '../lib/skilling/skills'; -import { convertXPtoLVL, getUsername } from '../lib/util'; -import { logError } from '../lib/util/logError'; +import { Prisma } from '@prisma/client'; +import PQueue from 'p-queue'; +import { partition } from 'remeda'; +import z from 'zod'; +import { type CommandResponse, Stopwatch, convertXPtoLVL, getUsernameSync, returnStringOrFile } from '../lib/util'; +import { ClueTiers } from './clues/clueTiers'; +import { loggedRawPrismaQuery } from './rawSql'; import { TeamLoot } from './simulation/TeamLoot'; +import { SkillsArray } from './skilling/types'; import type { ItemBank } from './types'; +import { fetchMultipleCLLeaderboards } from './util/clLeaderboard'; +import { logError } from './util/logError'; -function addToUserMap(userMap: Record, id: string, reason: string) { - if (!userMap[id]) userMap[id] = []; - userMap[id].push(reason); -} +const RoleResultSchema = z.object({ + roleID: z.string().min(17).max(19), + userID: z.string().min(17).max(19), + reason: z.string(), + badge: z.number().int().optional() +}); +type RoleResult = z.infer; const minigames = Minigames.map(game => game.column).filter(i => i !== 'tithe_farm'); -const collections = ['pets', 'skilling', 'clues', 'bosses', 'minigames', 'raids', 'slayer', 'other', 'custom']; - -for (const cl of collections) { +const CLS_THAT_GET_ROLE = [ + 'pets', + 'skilling', + 'clues', + 'bosses', + 'minigames', + 'raids', + 'slayer', + 'other', + 'custom', + 'overall' +]; + +for (const cl of CLS_THAT_GET_ROLE) { const items = getCollectionItems(cl); if (!items || items.length === 0) { throw new Error(`${cl} isn't a valid CL.`); } } -const mostSlayerPointsQuery = `SELECT id, 'Most Points' as desc -FROM users -WHERE "slayer.points" > 50 -ORDER BY "slayer.points" DESC -LIMIT 1;`; - -const longerSlayerTaskStreakQuery = `SELECT user_id::text as id, 'Longest Task Streak' as desc -FROM user_stats -WHERE "slayer_task_streak" > 20 -ORDER BY "slayer_task_streak" DESC -LIMIT 1;`; - -const mostSlayerTasksDoneQuery = `SELECT user_id::text as id, 'Most Tasks' as desc -FROM slayer_tasks -GROUP BY user_id -ORDER BY count(user_id)::int DESC -LIMIT 1;`; - -async function addRoles({ - users, - role, - badge, - userMap -}: { - users: string[]; - role: string; - badge: number | null; - userMap?: Record; -}): Promise { - if (process.env.TEST) return ''; - const g = globalClient.guilds.cache.get(SupportServer); - if (!g) throw new Error('No support guild'); - const added: string[] = []; - const removed: string[] = []; - const _role = await g.roles.fetch(role).catch(noOp); - if (!_role) return `\nCould not check ${role} role`; - for (const u of users.filter(notEmpty)) { - await g.members.fetch(u).catch(noOp); +async function topSkillers() { + const results: RoleResult[] = []; + + const [top200TotalXPUsers, ...top200ms] = await prisma.$transaction([ + prisma.$queryRawUnsafe( + `SELECT id, ${SkillsArray.map(s => `"skills.${s}"`)}, ${SkillsArray.map(s => `"skills.${s}"::bigint`).join( + ' + ' + )} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` + ), + ...SkillsArray.map(s => { + const query = `SELECT id, "skills.${s}" AS xp, '${s}' AS skill FROM users ORDER BY xp DESC LIMIT 1;`; + return prisma.$queryRawUnsafe<{ + id: string; + xp: string; + skill: string; + }>(query); + }) + ]); + + for (const { id, skill } of top200ms.flat()) { + results.push({ + userID: id, + roleID: Roles.TopSkiller, + reason: `Rank 1 ${skill} XP`, + badge: BadgesEnum.TopSkiller + }); } - const roleName = _role.name!; - const noChangeUserDescriptions: string[] = []; - for (const mem of g.members.cache.values()) { - const mUser = await mUserFetch(mem.user.id); - if (mem.roles.cache.has(role) && !users.includes(mem.user.id)) { - if (production) { - await mem.roles.remove(role).catch(noOp); - } - - if (badge && mUser.user.badges.includes(badge)) { - await mUser.update({ - badges: mUser.user.badges.filter(i => i !== badge) - }); - } - removed.push(mem.user.username); - } - if (users.includes(mem.user.id)) { - noChangeUserDescriptions.push(`${mem.user.username}`); - if (production && !mem.roles.cache.has(role)) { - added.push(mem.user.username); - await mem.roles.add(role).catch(noOp); - } - if (badge && !mUser.user.badges.includes(badge)) { - await mUser.update({ - badges: { - push: badge - } - }); + const rankOneTotal = top200TotalXPUsers + .map((u: any) => { + let totalLevel = 0; + for (const skill of SkillsArray) { + totalLevel += convertXPtoLVL(Number(u[`skills.${skill}` as keyof any]) as any); } - } - } - let str = `\n**${roleName}**`; - if (added.length > 0) { - str += `\nAdded to: ${added.join(', ')}.`; - } - if (removed.length > 0) { - str += `\nRemoved from: ${removed.join(', ')}.`; - } - if (userMap) { - const userArr = []; - for (const [id, arr] of Object.entries(userMap)) { - const username = await getUsername(id); - userArr.push(`${username}(${arr.join(', ')})`); - } - str += `\n${userArr.join(',')}`; - } - if (added.length > 0 || removed.length > 0) { - str += '\n'; - } else { - return `**No Changes:** ${str}`; - } - return str; -} - -export async function runRolesTask() { - const skillVals = Object.values(Skills); + return { + id: u.id, + totalLevel + }; + }) + .sort((a: any, b: any) => b.totalLevel - a.totalLevel)[0]; - const results: string[] = []; - const q = async (str: string) => { - const result = await prisma.$queryRawUnsafe(str).catch(err => { - logError(`This query failed: ${str}`, err); - return []; - }); - return result; - }; - - // Top Skillers - async function topSkillers() { - const topSkillers = ( - await Promise.all([ - ...skillVals.map(s => - q< - { - id: string; - xp: string; - }[] - >(`SELECT id, "skills.${s.id}" as xp FROM users ORDER BY xp DESC LIMIT 1;`) - ), - q< - { - id: string; - }[] - >( - `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals - .map(s => `"skills.${s.id}"::bigint`) - .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 1;` - ) - ]) - ).map(i => i[0]?.id); - - // Rank 1 Total Level - const rankOneTotal = ( - await q( - `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals - .map(s => `"skills.${s.id}"::bigint`) - .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` - ) - ) - .map((u: any) => { - let totalLevel = 0; - for (const skill of skillVals) { - totalLevel += convertXPtoLVL(Number(u[`skills.${skill.id}` as keyof any]) as any); - } - return { - id: u.id, - totalLevel - }; - }) - .sort((a: any, b: any) => b.totalLevel - a.totalLevel)[0]; - topSkillers.push(rankOneTotal.id); - - results.push(await addRoles({ users: topSkillers, role: Roles.TopSkiller, badge: 9 })); - } + results.push({ + userID: rankOneTotal.id, + roleID: Roles.TopSkiller, + reason: 'Rank 1 Total Level', + badge: BadgesEnum.TopSkiller + }); - // Top Collectors - async function topCollector() { - const userMap = {}; - - function generateQuery(items: number[], ironmenOnly: boolean, limit: number) { - const t = ` -SELECT id, (cardinality(u.cl_keys) - u.inverse_length) as qty - FROM ( - SELECT ARRAY(SELECT * FROM JSONB_OBJECT_KEYS("collectionLogBank")) "cl_keys", - id, "collectionLogBank", - cardinality(ARRAY(SELECT * FROM JSONB_OBJECT_KEYS("collectionLogBank" - array[${items - .map(i => `'${i}'`) - .join(', ')}]))) "inverse_length" - FROM users - WHERE "collectionLogBank" ?| array[${items.map(i => `'${i}'`).join(', ')}] - ${ironmenOnly ? 'AND "minion.ironman" = true' : ''} - ) u - ORDER BY qty DESC - LIMIT ${limit}; -`; - - return t; - } + return results; +} - const topCollectors = ( - await Promise.all( - collections.map(async clName => { - const items = getCollectionItems(clName); - if (!items || items.length === 0) { - logError(`${clName} collection log doesnt exist`); - return []; - } - - function handleErr(): any[] { - logError(`Failed to select top collectors for ${clName}`); - return []; - } - - const [users, ironUsers] = await Promise.all([ - q(generateQuery(items, false, 1)) - .then(i => i.filter((i: any) => i.qty > 0) as any[]) - .catch(handleErr), - q(generateQuery(items, true, 1)) - .then(i => i.filter((i: any) => i.qty > 0) as any[]) - .catch(handleErr) - ]); - - const result = []; - const userID = users[0]?.id; - const ironmanID = ironUsers[0]?.id; - - if (userID) { - addToUserMap(userMap, userID, `Rank 1 ${clName} CL`); - result.push(userID); - } - if (ironmanID) { - addToUserMap(userMap, ironmanID, `Rank 1 Ironman ${clName} CL`); - result.push(ironmanID); - } - - return result; - }) - ) - ).flat(2); - - const topIronUsers = (await q(generateQuery(getCollectionItems('overall'), true, 3))).filter( - (i: any) => i.qty > 0 - ) as any[]; - for (let i = 0; i < topIronUsers.length; i++) { - const id = topIronUsers[i]?.id; - addToUserMap(userMap, id, `Rank ${i + 1} Ironman Collector`); - topCollectors.push(id); - } - const topNormieUsers = (await q(generateQuery(getCollectionItems('overall'), false, 3))).filter( - (i: any) => i.qty > 0 - ) as any[]; - for (let i = 0; i < topNormieUsers.length; i++) { - const id = topNormieUsers[i]?.id; - addToUserMap(userMap, id, `Rank ${i + 1} Collector`); - topCollectors.push(id); +async function topCollector() { + const results: RoleResult[] = []; + const rankOneInSpecifiedCLs = await fetchMultipleCLLeaderboards( + CLS_THAT_GET_ROLE.map(cl => { + const items = getCollectionItems(cl); + const base = { + items, + clName: cl, + resultLimit: cl === 'overall' ? 3 : 1 + } as const; + return [ + { ...base, ironmenOnly: true }, + { ...base, ironmenOnly: false } + ]; + }).flat(2) + ); + for (const { users, clName, ironmenOnly } of rankOneInSpecifiedCLs) { + for (const user of users) { + results.push({ + userID: user.id, + roleID: Roles.TopCollector, + reason: `Rank 1 ${ironmenOnly ? 'Iron' : 'Main'} ${clName}`, + badge: BadgesEnum.TopCollector + }); } - - results.push(await addRoles({ users: topCollectors, role: Roles.TopCollector, badge: 10, userMap })); } + return results; +} - // Top sacrificers - async function topSacrificers() { - const userMap = {}; - const topSacrificers: string[] = []; - const mostValue = await q('SELECT id FROM users ORDER BY "sacrificedValue" DESC LIMIT 3;'); - for (let i = 0; i < 3; i++) { - if (mostValue[i] !== undefined) { - topSacrificers.push(mostValue[i].id); - addToUserMap(userMap, mostValue[i].id, `Rank ${i + 1} Sacrifice Value`); - } - } - const mostValueIronman = await q( - 'SELECT id FROM users WHERE "minion.ironman" = true ORDER BY "sacrificedValue" DESC LIMIT 1;' - ); - topSacrificers.push(mostValueIronman[0].id); - addToUserMap(userMap, mostValueIronman[0].id, 'Rank 1 Ironman Sacrificed Value'); - - const mostUniques = await q(`SELECT u.id, u.sacbanklength FROM ( +async function topSacrificers() { + const results: RoleResult[] = []; + const users = await prisma.$transaction([ + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>( + `SELECT id, 'Top 3' AS reason FROM users ORDER BY "sacrificedValue" DESC LIMIT 3;` + ), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>( + `SELECT id, 'Top Ironman' AS reason FROM users WHERE "minion.ironman" = true ORDER BY "sacrificedValue" DESC LIMIT 1;` + ), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>(`SELECT u.id, 'Top Uniques' AS reason FROM ( SELECT (SELECT COUNT(*)::int FROM JSONB_OBJECT_KEYS("sacrificed_bank")) sacbanklength, user_id::text as id FROM user_stats ) u -ORDER BY u.sacbanklength DESC LIMIT 1;`); - - const mostUniquesIron = await q(`SELECT u.id, u.sacbanklength FROM ( +ORDER BY u.sacbanklength DESC LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>(`SELECT u.id, 'Top Ironman Uniques' AS reason FROM ( SELECT (SELECT COUNT(*)::int FROM JSONB_OBJECT_KEYS("sacrificed_bank")) sacbanklength, user_id::text as id FROM user_stats INNER JOIN users ON "user_stats"."user_id"::text = "users"."id" WHERE "users"."minion.ironman" = true ) u -ORDER BY u.sacbanklength DESC LIMIT 1;`); - topSacrificers.push(mostUniques[0].id); - addToUserMap(userMap, mostUniques[0].id, 'Most Uniques Sacrificed'); - topSacrificers.push(mostUniquesIron[0].id); - addToUserMap(userMap, mostUniquesIron[0].id, 'Most Ironman Uniques Sacrificed'); - - results.push(await addRoles({ users: topSacrificers, role: Roles.TopSacrificer, badge: 8, userMap })); +ORDER BY u.sacbanklength DESC LIMIT 1;`) + ]); + + for (const res of users.flat()) { + results.push({ + userID: res.id, + reason: res.reason, + roleID: Roles.TopSacrificer, + badge: BadgesEnum.TopSacrifice + }); } - // Top minigamers - async function topMinigamers() { - const topMinigamers = ( - await Promise.all( - minigames.map(m => - q( - `SELECT user_id, '${m}' as m + return results; +} + +async function topMinigamers() { + const results: RoleResult[] = []; + const topMinigamers = await prisma.$transaction( + minigames.map(m => + prisma.$queryRawUnsafe<{ id: string; minigame_name: string; qty: number }[]>( + `SELECT user_id::text AS id, '${m}' AS minigame_name FROM minigames ORDER BY ${m} DESC LIMIT 1;` - ) - ) ) - ).map((i: any) => [i[0].user_id, Minigames.find(m => m.column === i[0].m)?.name]); - - const userMap = {}; - for (const [id, m] of topMinigamers) { - addToUserMap(userMap, id, `Rank 1 ${m}`); - } + ) + ); - results.push( - await addRoles({ - users: topMinigamers.map(i => i[0]), - role: Roles.TopMinigamer, - badge: 11, - userMap - }) - ); + for (const { id, minigame_name } of topMinigamers.flat()) { + results.push({ + userID: id, + roleID: Roles.TopMinigamer, + reason: `Rank 1 ${minigame_name}`, + badge: BadgesEnum.TopMinigame + }); } - // Top clue hunters - async function topClueHunters() { - const topClueHunters = ( - await Promise.all( - ClueTiers.map(t => - q( - `SELECT id, '${t.name}' as n, (openable_scores->>'${t.id}')::int as qty -FROM users -INNER JOIN "user_stats" ON "user_stats"."user_id"::text = "users"."id" + return results; +} + +async function topClueHunters() { + const results: RoleResult[] = []; + const topClueHunters = await prisma.$transaction( + ClueTiers.map(t => + prisma.$queryRawUnsafe<{ user_id: string; tier_name: string; qty: string }>( + ` +SELECT user_id::text, '${t.name}' AS tier_name, (openable_scores->>'${t.id}')::int AS qty +FROM user_stats WHERE "openable_scores"->>'${t.id}' IS NOT NULL ORDER BY qty DESC LIMIT 1;` - ) - ) ) ) - .filter((i: any) => Boolean(i[0]?.id)) - .map((i: any) => [i[0]?.id, i[0]?.n]); - - const userMap = {}; - - for (const [id, n] of topClueHunters) { - addToUserMap(userMap, id, `Rank 1 ${n} Clues`); - } + ); - results.push( - await addRoles({ - users: topClueHunters.map(i => i[0]), - role: Roles.TopClueHunter, - badge: null, - userMap - }) - ); + for (const res of topClueHunters.flat()) { + results.push({ + userID: res.user_id, + roleID: Roles.TopClueHunter, + reason: `Rank 1 ${res.tier_name} Clues`, + badge: BadgesEnum.TopMinigame + }); } + return results; +} - // Top farmers - async function farmers() { - const queries = [ - `SELECT id, 'Top 2 Farming Contracts' as desc +async function topFarmers() { + const results: RoleResult[] = []; + const queries = [ + `SELECT id, 'Top 2 Farming Contracts' as desc FROM users WHERE "minion.farmingContract" IS NOT NULL AND "minion.ironman" = true ORDER BY ("minion.farmingContract"->>'contractsCompleted')::int DESC LIMIT 2;`, - `SELECT id, 'Top 2 Ironman Farming Contracts' as desc + `SELECT id, 'Top 2 Ironman Farming Contracts' as desc FROM users WHERE "minion.farmingContract" IS NOT NULL ORDER BY ("minion.farmingContract"->>'contractsCompleted')::int DESC LIMIT 2;`, - `SELECT user_id::text as id, 'Top 2 Most Farming Trips' as desc + `SELECT user_id::text as id, 'Top 2 Most Farming Trips' as desc FROM activity WHERE type = 'Farming' GROUP BY user_id ORDER BY count(user_id)::int DESC LIMIT 2;`, - `SELECT user_id::text as id, 'Top 2 Tithe Farm' as desc + `SELECT user_id::text as id, 'Top 2 Tithe Farm' as desc FROM user_stats ORDER BY "tithe_farms_completed" DESC LIMIT 2;` - ]; - const res = (await Promise.all(queries.map(q))).map((i: any) => [i[0]?.id, i[0]?.desc]); - const userMap = {}; - for (const [id, desc] of res) { - addToUserMap(userMap, id, desc); - } - - results.push( - await addRoles({ - users: res.map(i => i[0]), - role: '894194027363205150', - badge: null, - userMap - }) - ); + ]; + const res = await prisma.$transaction(queries.map(q => prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(q))); + for (const { id, desc } of res.flat()) { + results.push({ + userID: id, + roleID: '894194027363205150', + reason: desc, + badge: BadgesEnum.Slayer + }); } + return results; +} - // Top slayers - async function slayer() { - const topSlayers = ( - await Promise.all( - [mostSlayerPointsQuery, longerSlayerTaskStreakQuery, mostSlayerTasksDoneQuery].map(query => q(query)) - ) - ) - .filter((i: any) => Boolean(i[0]?.id)) - .map((i: any) => [i[0]?.id, i[0]?.desc]); - - const userMap = {}; - for (const [id, desc] of topSlayers) { - addToUserMap(userMap, id, desc); - } - - results.push( - await addRoles({ - users: topSlayers.map(i => i[0]), - role: Roles.TopSlayer, - badge: null, - userMap - }) - ); +async function fetchSlayerResults() { + const results: RoleResult[] = []; + const topSlayers = await prisma.$transaction([ + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT id, 'Most Points' as desc +FROM users +WHERE "slayer.points" > 50 +ORDER BY "slayer.points" DESC +LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT user_id::text as id, 'Longest Task Streak' as desc +FROM user_stats +WHERE "slayer_task_streak" > 20 +ORDER BY "slayer_task_streak" DESC +LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT user_id::text as id, 'Most Tasks' as desc +FROM slayer_tasks +GROUP BY user_id +ORDER BY count(user_id)::int DESC +LIMIT 1;`) + ]); + + for (const { id, desc } of topSlayers.flat()) { + results.push({ + userID: id, + roleID: Roles.TopSlayer, + reason: desc, + badge: BadgesEnum.Slayer + }); } + return results; +} - // Top giveawayers - async function giveaways() { - const GIVEAWAY_CHANNELS = [ - '792691343284764693', - '732207379818479756', - '342983479501389826', - '982989775399174184', - '346304390858145792' - ]; - const res = await prisma.$queryRaw<{ user_id: string; qty: number }[]>`SELECT user_id, COUNT(user_id)::int AS qty -FROM giveaway -WHERE channel_id IN (${Prisma.join(GIVEAWAY_CHANNELS)}) -AND user_id NOT IN ('157797566833098752') -GROUP BY user_id -ORDER BY qty DESC -LIMIT 50;`; - const userIDsToCheck = res.map(i => i.user_id); - - const giveawayBank = new TeamLoot(); - - const giveaways = await prisma.giveaway.findMany({ - where: { - channel_id: { - in: GIVEAWAY_CHANNELS - }, - user_id: { - in: userIDsToCheck - } +async function giveaways() { + const results: RoleResult[] = []; + const GIVEAWAY_CHANNELS = [ + '792691343284764693', + '732207379818479756', + '342983479501389826', + '982989775399174184', + '346304390858145792' + ]; + const res = await prisma.$queryRaw<{ user_id: string; qty: number }[]>`SELECT user_id, COUNT(user_id)::int AS qty + FROM giveaway + WHERE channel_id IN (${Prisma.join(GIVEAWAY_CHANNELS)}) + AND user_id NOT IN ('157797566833098752') + GROUP BY user_id + ORDER BY qty DESC + LIMIT 50;`; + const userIDsToCheck = res.map(i => i.user_id); + + const giveawayBank = new TeamLoot(); + + const giveaways = await prisma.giveaway.findMany({ + where: { + channel_id: { + in: GIVEAWAY_CHANNELS + }, + user_id: { + in: userIDsToCheck } - }); - for (const giveaway of giveaways) { - giveawayBank.add(giveaway.user_id, giveaway.loot as ItemBank); } + }); - const userMap = {}; - const [[highestID, loot]] = giveawayBank.entries().sort((a, b) => b[1].value() - a[1].value()); - addToUserMap(userMap, highestID, `Most Value Given Away (${loot.value()})`); - - results.push( - await addRoles({ - users: [highestID], - role: '1052481561603346442', - badge: null, - userMap - }) - ); + if (giveaways.length === 0) return results; + + for (const giveaway of giveaways) { + giveawayBank.add(giveaway.user_id, giveaway.loot as ItemBank); } - // Global CL % - async function globalCL() { - const result = await roboChimpClient.$queryRaw<{ id: string; total_cl_percent: number }[]>`SELECT ((osb_cl_percent + bso_cl_percent) / 2) AS total_cl_percent, id::text AS id -FROM public.user -WHERE osb_cl_percent IS NOT NULL AND bso_cl_percent IS NOT NULL -ORDER BY total_cl_percent DESC -LIMIT 10;`; - - results.push( - await addRoles({ - users: result.slice(0, 10).map(i => i.id), - role: Roles.TopGlobalCL, - badge: null - }) - ); + const [[highestID, loot]] = giveawayBank.entries().sort((a, b) => b[1].value() - a[1].value()); + + results.push({ + userID: highestID, + roleID: '1052481561603346442', + reason: `Most Value Given Away (${loot.value()})`, + badge: BadgesEnum.TopGiveawayer + }); + return results; +} + +async function globalCL() { + const results: RoleResult[] = []; + const result = await roboChimpClient.$queryRaw<{ id: string; total_cl_percent: number }[]>`SELECT ((osb_cl_percent + bso_cl_percent) / 2) AS total_cl_percent, id::text AS id + FROM public.user + WHERE osb_cl_percent IS NOT NULL AND bso_cl_percent IS NOT NULL + ORDER BY total_cl_percent DESC + LIMIT 10;`; + + for (const user of result) { + results.push({ + userID: user.id, + roleID: Roles.TopGlobalCL, + reason: `Top Global CL ${user.total_cl_percent}%`, + badge: BadgesEnum.TopCollector + }); } + return results; +} + +export async function runRolesTask(dryRun: boolean): Promise { + const results: RoleResult[] = []; + + const promiseQueue = new PQueue({ concurrency: 2 }); const tup = [ - ['Top Slayer', slayer], + ['Top Slayer', fetchSlayerResults], ['Top Clue Hunters', topClueHunters], ['Top Minigamers', topMinigamers], ['Top Sacrificers', topSacrificers], ['Top Collectors', topCollector], ['Top Skillers', topSkillers], - ['Top Farmers', farmers], + ['Top Farmers', topFarmers], ['Top Giveawayers', giveaways], ['Global CL', globalCL] ] as const; - const failed: string[] = []; - await Promise.all( - tup.map(async ([name, fn]) => { + for (const [name, fn] of tup) { + promiseQueue.add(async () => { + const stopwatch = new Stopwatch(); try { - await fn(); - } catch (err: any) { - if (process.env.TEST) { - throw err; + const res = await fn(); + const [validResults, invalidResults] = partition(res, i => RoleResultSchema.safeParse(i).success); + results.push(...validResults); + if (invalidResults.length > 0) { + logError(`[RolesTask] Invalid results for ${name}: ${JSON.stringify(invalidResults)}`); } - failed.push(`${name} (${err.message})`); - logError(err); + } catch (err) { + logError(`[RolesTask] Error in ${name}: ${err}`); + } finally { + debugLog(`[RolesTask] Ran ${name} in ${stopwatch.stop()}`); } - }) - ); + }); + } + + await promiseQueue.onIdle(); + + debugLog(`Finished role functions, ${results.length} results`); + + const allBadgeIDs = uniqueArr(results.map(i => i.badge)).filter(notEmpty); + const allRoleIDs = uniqueArr(results.map(i => i.roleID)).filter(notEmpty); + + if (!dryRun) { + const roleNames = new Map(); + const supportServerGuild = globalClient.guilds.cache.get(SupportServer)!; + if (!supportServerGuild) throw new Error('No support guild'); + + // Remove all top badges from all users (and add back later) + debugLog('Removing badges...'); + const badgeIDs = `ARRAY[${allBadgeIDs.join(',')}]`; + await loggedRawPrismaQuery(` +UPDATE users +SET badges = badges - ${badgeIDs} +WHERE badges && ${badgeIDs} +`); + + // Remove roles from ineligible users + debugLog('Remove roles from ineligible users...'); + for (const member of supportServerGuild.members.cache.values()) { + const rolesToRemove = member.roles.cache.filter(r => allRoleIDs.includes(r.id)); + if (rolesToRemove.size > 0) { + await member.roles.remove(rolesToRemove.map(r => r.id)).catch(console.error); + } + } - const res = `**Roles** -${results.join('\n')} -${failed.length > 0 ? `Failed: ${failed.join(', ')}` : ''}`; + // Add roles to users + debugLog('Add roles to users...'); + for (const { userID, roleID, badge } of results) { + if (!userID) continue; + const role = await supportServerGuild.roles.fetch(roleID).catch(console.error); + const member = await supportServerGuild.members.fetch(userID).catch(noOp); + if (!member) { + debugLog(`Failed to find member ${userID}`); + continue; + } + if (!role) { + debugLog(`Failed to find role ${roleID}`); + continue; + } + roleNames.set(roleID, role.name); + + if (!member.roles.cache.has(roleID)) { + await member.roles.add(roleID).catch(console.error); + } + + if (badge) { + const user = await mUserFetch(userID); + if (!user.user.badges.includes(badge)) { + await user.update({ + badges: { + push: badge + } + }); + } + } + } + + return returnStringOrFile( + `**Roles**\n${results.map(r => `${getUsernameSync(r.userID)} got ${roleNames.get(r.roleID)} because ${r.reason}`).join('\n')}` + ); + } - return res; + return 'Dry run'; } diff --git a/src/lib/simulation/nex.ts b/src/lib/simulation/nex.ts index a5f928a3d3..34f649c2a0 100644 --- a/src/lib/simulation/nex.ts +++ b/src/lib/simulation/nex.ts @@ -59,7 +59,7 @@ export function checkNexUser(user: MUser): [false] | [true, string] { if (!user.hasSkillReqs(minStats)) { return [true, `${tag} doesn't have the skill requirements: ${formatSkillRequirements(minStats)}.`]; } - if (user.GP < 1_000_000) return [true, `${tag} needs atleast 1m GP to cover potential deaths.`]; + if (user.GP < 1_000_000) return [true, `${tag} needs at least 1m GP to cover potential deaths.`]; const { offence, defence, rangeGear } = nexGearStats(user); if (offence < 50) { return [ diff --git a/src/lib/simulation/toa.ts b/src/lib/simulation/toa.ts index 8e0d27684f..810b071f63 100644 --- a/src/lib/simulation/toa.ts +++ b/src/lib/simulation/toa.ts @@ -247,7 +247,7 @@ const toaRequirements: { return true; }, desc: () => - `atleast ${BP_DARTS_NEEDED}x darts per raid, and using one of: ${ALLOWED_DARTS.map(i => i.name).join( + `at least ${BP_DARTS_NEEDED}x darts per raid, and using one of: ${ALLOWED_DARTS.map(i => i.name).join( ', ' )}, loaded in Blowpipe` }, @@ -278,7 +278,7 @@ const toaRequirements: { return true; }, desc: () => - `decent range gear (BiS is ${maxRangeGear.toString()}), atleast ${BOW_ARROWS_NEEDED}x arrows equipped, and one of these bows: ${REQUIRED_RANGE_WEAPONS.map( + `decent range gear (BiS is ${maxRangeGear.toString()}), at least ${BOW_ARROWS_NEEDED}x arrows equipped, and one of these bows: ${REQUIRED_RANGE_WEAPONS.map( itemNameFromID ).join(', ')}` }, @@ -335,11 +335,11 @@ const toaRequirements: { minimumSuppliesNeeded = minSuppliesWithAtkStr; } if (!user.owns(minimumSuppliesNeeded.clone().multiply(quantity))) { - return `You need atleast this much supplies: ${minimumSuppliesNeeded}.`; + return `You need at least this much supplies: ${minimumSuppliesNeeded}.`; } const bfCharges = BLOOD_FURY_CHARGES_PER_RAID * quantity; if (user.gear.melee.hasEquipped('Amulet of blood fury') && user.user.blood_fury_charges < bfCharges) { - return `You need atleast ${bfCharges} Blood fury charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${bfCharges} Blood fury charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' @@ -348,7 +348,7 @@ const toaRequirements: { const tumCharges = TUMEKEN_SHADOW_PER_RAID * quantity; if (user.gear.mage.hasEquipped("Tumeken's shadow") && user.user.tum_shadow_charges < tumCharges) { - return `You need atleast ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' @@ -357,7 +357,7 @@ const toaRequirements: { return true; }, - desc: () => `Need atleast ${minimumSuppliesNeeded}` + desc: () => `Need at least ${minimumSuppliesNeeded}` }, { name: 'Rune Pouch', @@ -368,7 +368,7 @@ const toaRequirements: { } return true; }, - desc: () => `Need atleast ${minimumSuppliesNeeded}` + desc: () => `Need at least ${minimumSuppliesNeeded}` }, { name: 'Poison Protection', @@ -1044,7 +1044,7 @@ async function checkTOAUser( true, `${ user.usernameOrMention - } doesn't have enough Serpentine helm charges. You need atleast ${serpHelmCharges} charges to do a ${formatDuration( + } doesn't have enough Serpentine helm charges. You need at least ${serpHelmCharges} charges to do a ${formatDuration( duration )} TOA raid.` ]; @@ -1057,7 +1057,7 @@ async function checkTOAUser( if (kc < dividedRaidLevel) { return [ true, - `${user.usernameOrMention}, you need atleast ${dividedRaidLevel} TOA KC to ${ + `${user.usernameOrMention}, you need at least ${dividedRaidLevel} TOA KC to ${ teamSize === 2 ? 'duo' : 'solo' } a level ${raidLevel} TOA raid.` ]; diff --git a/src/lib/skilling/functions/calcsFarming.ts b/src/lib/skilling/functions/calcsFarming.ts index 170e12dd98..a234f023fe 100644 --- a/src/lib/skilling/functions/calcsFarming.ts +++ b/src/lib/skilling/functions/calcsFarming.ts @@ -1,5 +1,6 @@ import { randInt } from 'e'; +import { QuestID } from '../../minions/data/quests'; import type { Plant } from '../types'; import { SkillsEnum } from '../types'; @@ -28,6 +29,19 @@ export function calcNumOfPatches(plant: Plant, user: MUser, qp: number): [number break; } } + + if (user.user.finished_quest_ids.includes(QuestID.ChildrenOfTheSun)) { + switch (plant.seedType) { + case 'allotment': + numOfPatches += 2; + break; + case 'herb': + case 'flower': + numOfPatches += 1; + break; + } + } + return [numOfPatches]; } diff --git a/src/lib/skilling/types.ts b/src/lib/skilling/types.ts index 5d08411b7b..364f407eff 100644 --- a/src/lib/skilling/types.ts +++ b/src/lib/skilling/types.ts @@ -313,7 +313,7 @@ export enum HunterTechniqueEnum { BoxTrapping = 'box trapping', ButterflyNetting = 'butterfly netting', DeadfallTrapping = 'deadfall trapping', - Falconry = 'falconry', + Falconry = 'hawking', MagicBoxTrapping = 'magic box trapping', NetTrapping = 'net trapping', PitfallTrapping = 'pitfall trapping', diff --git a/src/lib/slayer/slayerUtil.ts b/src/lib/slayer/slayerUtil.ts index 8ed9e4cdbb..c1d9d5cc4e 100644 --- a/src/lib/slayer/slayerUtil.ts +++ b/src/lib/slayer/slayerUtil.ts @@ -34,42 +34,53 @@ interface DetermineBoostParams { cbOpts: CombatOptionsEnum[]; user: MUser; monster: KillableMonster; - method?: PvMMethod | null; + methods?: PvMMethod[] | null; isOnTask?: boolean; wildyBurst?: boolean; } -export function determineBoostChoice(params: DetermineBoostParams) { - let boostChoice = 'none'; +export function determineCombatBoosts(params: DetermineBoostParams) { + // if EHP slayer (PvMMethod) the methods are initialized with boostMethods variable + const boostMethods = (params.methods ?? ['none']).flat().filter(method => method); - if (params.method && params.method === 'none') { - return boostChoice; + // check if user has cannon combat option turned on + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysCannon)) { + boostMethods.includes('cannon') ? null : boostMethods.push('cannon'); } - if (params.method && params.method === 'chinning') { - boostChoice = 'chinning'; - } else if (params.method && params.method === 'barrage') { - boostChoice = 'barrage'; - } else if (params.method && params.method === 'burst') { - boostChoice = 'burst'; - } else if (params.method && params.method === 'cannon') { - boostChoice = 'cannon'; - } else if ( - params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage) && - (params.monster?.canBarrage || params.wildyBurst) - ) { - boostChoice = 'barrage'; - } else if ( - params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst) && - (params.monster?.canBarrage || params.wildyBurst) - ) { - boostChoice = 'burst'; - } else if (params.cbOpts.includes(CombatOptionsEnum.AlwaysCannon)) { - boostChoice = 'cannon'; + + // check for special burst case under wildyBurst variable + if (params.wildyBurst) { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } } - if (boostChoice === 'barrage' && params.user.skillLevel(SkillsEnum.Magic) < 94) { - boostChoice = 'burst'; + // check if the monster can be barraged + if (params.monster.canBarrage) { + // check if the monster exists in catacombs + if (params.monster.existsInCatacombs) { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } + } else if (!params.monster.cannonMulti) { + // prevents cases such as: cannoning in singles but receiving multi combat bursting boost + return boostMethods; + } else { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } + } } - return boostChoice; + + return boostMethods; } export async function calculateSlayerPoints(currentStreak: number, master: SlayerMaster, user: MUser) { diff --git a/src/lib/startupScripts.ts b/src/lib/startupScripts.ts index 1c279f0b4a..fab5ab93fe 100644 --- a/src/lib/startupScripts.ts +++ b/src/lib/startupScripts.ts @@ -1,34 +1,61 @@ import { Items } from 'oldschooljs'; +import { globalConfig } from './constants'; const startupScripts: { sql: string; ignoreErrors?: true }[] = []; -const arrayColumns = [ - ['guilds', 'disabledCommands'], - ['guilds', 'staffOnlyChannels'], - ['users', 'badges'], - ['users', 'bitfield'], - ['users', 'favoriteItems'], - ['users', 'favorite_alchables'], - ['users', 'favorite_food'], - ['users', 'favorite_bh_seeds'], - ['users', 'attack_style'], - ['users', 'combat_options'], - ['users', 'slayer.unlocks'], - ['users', 'slayer.blocked_ids'], - ['users', 'slayer.autoslay_options'] -]; +startupScripts.push({ + sql: `CREATE OR REPLACE FUNCTION add_item_to_bank( + bank JSONB, + key TEXT, + quantity INT +) RETURNS JSONB LANGUAGE plpgsql AS $$ +BEGIN + RETURN ( + CASE + WHEN bank ? key THEN + jsonb_set( + bank, + ARRAY[key], + to_jsonb((bank->>key)::INT + quantity) + ) + ELSE + jsonb_set( + bank, + ARRAY[key], + to_jsonb(quantity) + ) + END + ); +END; +$$;` +}); -for (const [table, column] of arrayColumns) { - startupScripts.push({ - sql: `UPDATE "${table}" SET "${column}" = '{}' WHERE "${column}" IS NULL;` - }); - startupScripts.push({ - sql: ` -ALTER TABLE "${table}" - ALTER COLUMN "${column}" SET DEFAULT '{}', - ALTER COLUMN "${column}" SET NOT NULL;` - }); -} +startupScripts.push({ + sql: `CREATE OR REPLACE FUNCTION remove_item_from_bank( + bank JSONB, + key TEXT, + quantity INT +) RETURNS JSONB LANGUAGE plpgsql AS $$ +DECLARE + current_value INT; +BEGIN + IF bank ? key THEN + current_value := (bank->>key)::INT - quantity; + IF current_value > 0 THEN + RETURN jsonb_set( + bank, + ARRAY[key], + to_jsonb(current_value) + ); + ELSE + RETURN bank - key; + END IF; + ELSE + RETURN bank; + END IF; +END; +$$;` +}); interface CheckConstraint { table: string; @@ -37,24 +64,12 @@ interface CheckConstraint { body: string; } const checkConstraints: CheckConstraint[] = [ - { - table: 'users', - column: 'lms_points', - name: 'users_lms_points_min', - body: 'lms_points >= 0' - }, { table: 'users', column: '"GP"', name: 'users_gp', body: '"GP" >= 0' }, - { - table: 'users', - column: '"QP"', - name: 'users_qp', - body: '"QP" >= 0' - }, { table: 'ge_listing', column: 'asking_price_per_item', @@ -152,8 +167,9 @@ DO UPDATE SET name = EXCLUDED.name WHERE item_metadata.name IS DISTINCT FROM EXCLUDED.name; `; - -startupScripts.push({ sql: itemMetaDataQuery }); +if (globalConfig.isProduction) { + startupScripts.push({ sql: itemMetaDataQuery }); +} export async function runStartupScripts() { await prisma.$transaction(startupScripts.map(query => prisma.$queryRawUnsafe(query.sql))); diff --git a/src/lib/tickers.ts b/src/lib/tickers.ts index d5555cd497..d94ad99023 100644 --- a/src/lib/tickers.ts +++ b/src/lib/tickers.ts @@ -3,6 +3,7 @@ import type { TextChannel } from 'discord.js'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; import { Time, noOp, randInt, removeFromArr, shuffleArr } from 'e'; +import { TimerManager } from '@sapphire/timer-manager'; import { production } from '../config'; import { userStatsUpdate } from '../mahoji/mahojiSettings'; import { mahojiUserSettingsUpdate } from './MUser'; @@ -221,7 +222,6 @@ WHERE bitfield && '{2,3,4,5,6,7,8,12,21,24}'::int[] AND user_stats."last_daily_t BitField.IsPatronTier4, BitField.IsPatronTier5, BitField.IsPatronTier6, - BitField.isContributor, BitField.isModerator ] } @@ -398,10 +398,11 @@ export function initTickers() { logError(err); debugLog(`${ticker.name} ticker errored`, { type: 'TICKER' }); } finally { - ticker.timer = setTimeout(fn, ticker.interval); + if (ticker.timer) TimerManager.clearTimeout(ticker.timer); + ticker.timer = TimerManager.setTimeout(fn, ticker.interval); } }; - setTimeout(() => { + ticker.timer = TimerManager.setTimeout(() => { fn(); }, ticker.startupWait ?? 1); } diff --git a/src/lib/types/minions.ts b/src/lib/types/minions.ts index 13d9532c7c..26d33f24a2 100644 --- a/src/lib/types/minions.ts +++ b/src/lib/types/minions.ts @@ -454,6 +454,9 @@ export interface ColoTaskOptions extends ActivityTaskOptions { diedAt?: number; loot?: ItemBank; maxGlory: number; + scytheCharges: number; + venatorBowCharges: number; + bloodFuryCharges: number; } type UserID = string; diff --git a/src/lib/util/addSubTaskToActivityTask.ts b/src/lib/util/addSubTaskToActivityTask.ts index e717d57794..8e0fe2f06c 100644 --- a/src/lib/util/addSubTaskToActivityTask.ts +++ b/src/lib/util/addSubTaskToActivityTask.ts @@ -39,24 +39,28 @@ export default async function addSubTaskToActivityTask { +export const syncActiveUserIDs = async () => { const users = await prisma.$queryRawUnsafe< { user_id: string }[] >(`SELECT DISTINCT(${Prisma.ActivityScalarFieldEnum.user_id}::text) @@ -25,7 +25,7 @@ WHERE perk_tier > 0;`); CACHED_ACTIVE_USER_IDS.add(id); } debugLog(`${CACHED_ACTIVE_USER_IDS.size} cached active user IDs`); -}); +}; export function memoryAnalysis() { const guilds = globalClient.guilds.cache.size; diff --git a/src/lib/util/chatHeadImage.ts b/src/lib/util/chatHeadImage.ts index 36c117262c..0aee1092b1 100644 --- a/src/lib/util/chatHeadImage.ts +++ b/src/lib/util/chatHeadImage.ts @@ -34,7 +34,7 @@ const names: Record = { santa: 'Santa', izzy: "Cap'n Izzy No-Beard", alry: 'Alry the Angler', - ketKeh: 'Tzhaar-Ket-Keh', + ketKeh: 'TzHaar-Ket-Keh', gertrude: 'Gertrude', antiSanta: 'Anti-Santa', bunny: 'Easter Bunny', diff --git a/src/lib/util/clLeaderboard.ts b/src/lib/util/clLeaderboard.ts index 43def04ef5..175a9c5596 100644 --- a/src/lib/util/clLeaderboard.ts +++ b/src/lib/util/clLeaderboard.ts @@ -1,55 +1,64 @@ -import type { UserEvent } from '@prisma/client'; - +import { stringMatches } from '@oldschoolgg/toolkit'; import { userEventsToMap } from './userEvents'; -export async function fetchCLLeaderboard({ - ironmenOnly, - items, - resultLimit, - method = 'cl_array', - userEvents -}: { - ironmenOnly: boolean; - items: number[]; - resultLimit: number; - method?: 'cl_array' | 'raw_cl'; - userEvents: UserEvent[] | null; -}) { - const userEventMap = userEventsToMap(userEvents); - const userIds = Array.from(userEventMap.keys()); - if (method === 'cl_array') { - const userIdsList = userIds.length > 0 ? userIds.map(i => `'${i}'`).join(', ') : 'NULL'; - const specificUsers = - userIds.length > 0 - ? await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(cl_array) - CARDINALITY(cl_array - array[${items - .map(i => `${i}`) - .join(', ')}]) AS qty - FROM user_stats s INNER JOIN users u ON s.user_id::text = u.id - WHERE ${ironmenOnly ? 'u."minion.ironman" = true AND' : ''} user_id::text IN (${userIdsList}) -`) - : []; - const generalUsers = await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(cl_array) - CARDINALITY(cl_array - array[${items - .map(i => `${i}`) - .join(', ')}]) AS qty - FROM user_stats - ${ironmenOnly ? 'INNER JOIN "users" on "users"."id" = "user_stats"."user_id"::text' : ''} - WHERE (cl_array && array[${items.map(i => `${i}`).join(', ')}] - ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) - ${userIds.length > 0 ? `AND user_id::text NOT IN (${userIdsList})` : ''} - ORDER BY qty DESC - LIMIT ${resultLimit} -`); +export async function fetchMultipleCLLeaderboards( + leaderboards: { + ironmenOnly: boolean; + items: number[]; + resultLimit: number; + clName: string; + }[] +) { + const userEvents = await prisma.userEvent.findMany({ + where: { + type: 'CLCompletion' + }, + orderBy: { + date: 'asc' + } + }); + const parsedLeaderboards = leaderboards.map(l => { + const userEventMap = userEventsToMap( + userEvents.filter(e => e.collection_log_name && stringMatches(e.collection_log_name, l.clName)) + ); + return { + ...l, + userEventMap + }; + }); + + const results = await prisma.$transaction([ + ...parsedLeaderboards.map(({ items, userEventMap, ironmenOnly, resultLimit }) => { + const SQL_ITEMS = `ARRAY[${items.map(i => `${i}`).join(', ')}]`; + const userIds = Array.from(userEventMap.keys()); + const userIdsList = userIds.length > 0 ? userIds.map(i => `'${i}'`).join(', ') : 'NULL'; + + const query = ` +SELECT id, qty +FROM ( + SELECT id, CARDINALITY(cl_array & ${SQL_ITEMS}) AS qty + FROM users + WHERE (cl_array && ${SQL_ITEMS} + ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) ${userIds.length > 0 ? `OR id IN (${userIdsList})` : ''} + ) AS subquery +ORDER BY qty DESC +LIMIT ${resultLimit}; +`; + return prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(query); + }) + ]); - const users = [...specificUsers, ...generalUsers] + return results.map((res, index) => { + const src = parsedLeaderboards[index]; + + const users = res .sort((a, b) => { const valueDifference = b.qty - a.qty; if (valueDifference !== 0) { return valueDifference; } - const dateA = userEventMap.get(a.id); - const dateB = userEventMap.get(b.id); + const dateA = src.userEventMap.get(a.id); + const dateB = src.userEventMap.get(b.id); if (dateA && dateB) { return dateA - dateB; } @@ -62,30 +71,26 @@ export async function fetchCLLeaderboard({ return 0; }) .filter(i => i.qty > 0); - return users; - } - const users = ( - await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` -SELECT id, (cardinality(u.cl_keys) - u.inverse_length) as qty - FROM ( - SELECT array(SELECT * FROM jsonb_object_keys("collectionLogBank")) "cl_keys", - id, "collectionLogBank", - cardinality(array(SELECT * FROM jsonb_object_keys("collectionLogBank" - array[${items - .map(i => `'${i}'`) - .join(', ')}]))) "inverse_length" - FROM users - WHERE ("collectionLogBank" ?| array[${items.map(i => `'${i}'`).join(', ')}] - ${ironmenOnly ? 'AND "minion.ironman" = true' : ''}) - ${ - userIds.length > 0 - ? `OR (id in (${userIds.map(i => `'${i}'`).join(', ')})${ironmenOnly ? 'AND "minion.ironman" = true' : ''})` - : '' - } -) u -ORDER BY qty DESC -LIMIT ${resultLimit}; -`) - ).filter(i => i.qty > 0); - return users; + return { + ...src, + users + }; + }); +} + +export async function fetchCLLeaderboard({ + ironmenOnly, + items, + resultLimit, + clName +}: { + ironmenOnly: boolean; + items: number[]; + resultLimit: number; + method?: 'cl_array'; + clName: string; +}) { + const result = await fetchMultipleCLLeaderboards([{ ironmenOnly, items, resultLimit, clName }]); + return result[0]; } diff --git a/src/lib/util/webhook.ts b/src/lib/util/webhook.ts index 1582226f02..165d4d7488 100644 --- a/src/lib/util/webhook.ts +++ b/src/lib/util/webhook.ts @@ -1,7 +1,6 @@ import { channelIsSendable, splitMessage } from '@oldschoolgg/toolkit'; import type { AttachmentBuilder, BaseMessageOptions, EmbedBuilder, Message } from 'discord.js'; import { PartialGroupDMChannel, PermissionsBitField, WebhookClient } from 'discord.js'; -import PQueue from 'p-queue'; import { production } from '../../config'; import { logError } from './logError'; @@ -43,8 +42,6 @@ async function deleteWebhook(channelID: string) { await prisma.webhook.delete({ where: { channel_id: channelID } }); } -const queue = new PQueue({ concurrency: 10 }); - export async function sendToChannelID( channelID: string, data: { @@ -59,37 +56,14 @@ export async function sendToChannelID( const allowedMentions = data.allowedMentions ?? { parse: ['users'] }; - async function queuedFn() { - const channel = await resolveChannel(channelID); - if (!channel) return; + const channel = await resolveChannel(channelID); + if (!channel) return; - const files = data.image ? [data.image] : data.files; - const embeds = []; - if (data.embed) embeds.push(data.embed); - if (channel instanceof WebhookClient) { - try { - await sendToChannelOrWebhook(channel, { - content: data.content, - files, - embeds, - components: data.components, - allowedMentions - }); - } catch (err: any) { - const error = err as Error; - if (error.message === 'Unknown Webhook') { - await deleteWebhook(channelID); - await sendToChannelID(channelID, data); - } else { - logError(error, { - content: data.content ?? 'None', - channelID - }); - } - } finally { - channel.destroy(); - } - } else { + const files = data.image ? [data.image] : data.files; + const embeds = []; + if (data.embed) embeds.push(data.embed); + if (channel instanceof WebhookClient) { + try { await sendToChannelOrWebhook(channel, { content: data.content, files, @@ -97,9 +71,29 @@ export async function sendToChannelID( components: data.components, allowedMentions }); + } catch (err: any) { + const error = err as Error; + if (error.message === 'Unknown Webhook') { + await deleteWebhook(channelID); + await sendToChannelID(channelID, data); + } else { + logError(error, { + content: data.content ?? 'None', + channelID + }); + } + } finally { + channel.destroy(); } + } else { + await sendToChannelOrWebhook(channel, { + content: data.content, + files, + embeds, + components: data.components, + allowedMentions + }); } - return queue.add(queuedFn); } async function sendToChannelOrWebhook(channel: WebhookClient | Message['channel'], input: BaseMessageOptions) { diff --git a/src/mahoji/commands/admin.ts b/src/mahoji/commands/admin.ts index e60ff52cb1..27bb5bdfef 100644 --- a/src/mahoji/commands/admin.ts +++ b/src/mahoji/commands/admin.ts @@ -482,11 +482,6 @@ export const adminCommand: OSBMahojiCommand = { } ] }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'sync_roles', - description: 'Sync roles' - }, { type: ApplicationCommandOptionType.Subcommand, name: 'badges', @@ -665,7 +660,6 @@ export const adminCommand: OSBMahojiCommand = { sync_blacklist?: {}; loot_track?: { name: string }; cancel_task?: { user: MahojiUserOption }; - sync_roles?: {}; badges?: { user: MahojiUserOption; add?: string; remove?: string }; bypass_age?: { user: MahojiUserOption }; command?: { enable?: string; disable?: string }; @@ -697,20 +691,6 @@ export const adminCommand: OSBMahojiCommand = { minionActivityCacheDelete(user.id); return 'Done.'; } - if (options.sync_roles) { - // try { - // const result = await runRolesTask(); - // if (result.length < 2000) return result; - // return { - // content: 'The result was too big! Check the file.', - // files: [new AttachmentBuilder(Buffer.from(result), { name: 'roles.txt' })] - // }; - // } catch (err: any) { - // logError(err); - // return `Failed to run roles task. ${err.message}`; - // } - return 'The roles task is disabled for now.'; - } if (options.badges) { if ((!options.badges.remove && !options.badges.add) || (options.badges.add && options.badges.remove)) { @@ -883,7 +863,8 @@ export const adminCommand: OSBMahojiCommand = { ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); - process.exit(); + import('exit-hook').then(({ gracefulExit }) => gracefulExit(1)); + return 'Turning off...'; } if (options.shut_down) { debugLog('SHUTTING DOWN'); @@ -893,13 +874,14 @@ ${META_CONSTANTS.RENDERED_STR}` content: `Shutting down in ${dateFm(new Date(Date.now() + timer))}.` }); await economyLog('Flushing economy log due to shutdown', true); - await Promise.all([sleep(timer), GrandExchange.queue.onEmpty()]); + await Promise.all([sleep(timer), GrandExchange.queue.onIdle()]); await sendToChannelID(Channel.GeneralChannel, { content: `I am shutting down! Goodbye :( ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); - process.exit(0); + import('exit-hook').then(({ gracefulExit }) => gracefulExit(0)); + return 'Turning off...'; } if (options.sync_blacklist) { @@ -1004,7 +986,7 @@ ${guildCommands.length} Guild commands`; FROM users WHERE bank->>'${item.id}' IS NOT NULL;`); return `There are ${ownedResult[0].qty.toLocaleString()} ${item.name} owned by everyone. -There are ${await countUsersWithItemInCl(item.id, isIron)} ${isIron ? 'ironmen' : 'people'} with atleast 1 ${ +There are ${await countUsersWithItemInCl(item.id, isIron)} ${isIron ? 'ironmen' : 'people'} with at least 1 ${ item.name } in their collection log.`; } diff --git a/src/mahoji/commands/bingo.ts b/src/mahoji/commands/bingo.ts index 6f56fdc656..0f73adc593 100644 --- a/src/mahoji/commands/bingo.ts +++ b/src/mahoji/commands/bingo.ts @@ -675,7 +675,7 @@ export const bingoCommand: OSBMahojiCommand = { const fee = BOT_TYPE === 'OSB' ? 20_000_000 : 50_000_000; const creationCost = new Bank().add('Coins', fee); if (user.GP < creationCost.amount('Coins')) { - return `You need atleast ${creationCost} to create a bingo.`; + return `You need at least ${creationCost} to create a bingo.`; } const channel = globalClient.channels.cache.get(options.create_bingo.notifications_channel_id); @@ -713,9 +713,9 @@ export const bingoCommand: OSBMahojiCommand = { return 'Team size must be between 1 and 5.'; } - // Start date must be atleast 3 hours into the future + // Start date must be at least 3 hours into the future if (createOptions.start_date.getTime() < Date.now() + Time.Minute * 3) { - return 'Start date must be atleast 3 minutes into the future.'; + return 'Start date must be at least 3 minutes into the future.'; } // Start date cannot be more than 31 days into the future @@ -914,7 +914,7 @@ Example: \`add_tile:Coal|Trout|Egg\` is a tile where you have to receive a coal const cost = new Bank().add('Coins', amount); if (user.GP < cost.amount('Coins')) { - return `You need atleast ${cost} to add that much GP to the prize pool.`; + return `You need at least ${cost} to add that much GP to the prize pool.`; } await handleMahojiConfirmation( diff --git a/src/mahoji/commands/config.ts b/src/mahoji/commands/config.ts index 5be1c602b3..9a79f21fd9 100644 --- a/src/mahoji/commands/config.ts +++ b/src/mahoji/commands/config.ts @@ -638,6 +638,7 @@ export async function pinTripCommand( ) { if (!tripId) return 'Invalid trip.'; const id = Number(tripId); + if (!id || Number.isNaN(id)) return 'Invalid trip.'; const trip = await prisma.activity.findFirst({ where: { id, user_id: BigInt(user.id) } }); if (!trip) return 'Invalid trip.'; @@ -1072,7 +1073,7 @@ export const configCommand: OSBMahojiCommand = { >(` SELECT DISTINCT ON (activity.type) activity.type, activity.data, activity.id, activity.finish_date FROM activity -WHERE finish_date::date > now() - INTERVAL '31 days' +WHERE finish_date > now() - INTERVAL '14 days' AND user_id = '${user.id}'::bigint ORDER BY activity.type, finish_date DESC LIMIT 20; diff --git a/src/mahoji/commands/drycalc.ts b/src/mahoji/commands/drycalc.ts index 1975749cf5..dbc12697d6 100644 --- a/src/mahoji/commands/drycalc.ts +++ b/src/mahoji/commands/drycalc.ts @@ -39,7 +39,7 @@ export const dryCalcCommand: OSBMahojiCommand = { )}%** chance of not receiving any drop, and a **${round( dropChance, 2 - )}%** chance of receiving atleast one drop.`; + )}%** chance of receiving at least one drop.`; return output; } diff --git a/src/mahoji/commands/hunt.ts b/src/mahoji/commands/hunt.ts index 64d6d7e1ff..883a735cd4 100644 --- a/src/mahoji/commands/hunt.ts +++ b/src/mahoji/commands/hunt.ts @@ -280,8 +280,8 @@ export const huntCommand: OSBMahojiCommand = { type: 'Hunter' }); - let response = `${user.minionName} is now ${crystalImpling ? 'hunting' : `${creature.huntTechnique}`} ${ - crystalImpling ? '' : ` ${quantity}x ` + let response = `${user.minionName} is now ${crystalImpling ? 'hunting' : `${creature.huntTechnique}`}${ + crystalImpling ? ' ' : ` ${quantity}x ` }${creature.name}, it'll take around ${formatDuration(duration)} to finish.`; if (boosts.length > 0) { diff --git a/src/mahoji/commands/laps.ts b/src/mahoji/commands/laps.ts index 2e7961b462..45585d18b8 100644 --- a/src/mahoji/commands/laps.ts +++ b/src/mahoji/commands/laps.ts @@ -127,7 +127,7 @@ export const lapsCommand: OSBMahojiCommand = { } if (course.qpRequired && user.QP < course.qpRequired) { - return `You need atleast ${course.qpRequired} Quest Points to do this course.`; + return `You need at least ${course.qpRequired} Quest Points to do this course.`; } const maxTripLength = calcMaxTripLength(user, 'Agility'); diff --git a/src/mahoji/commands/leaderboard.ts b/src/mahoji/commands/leaderboard.ts index f1c2e39cd4..f98a813caf 100644 --- a/src/mahoji/commands/leaderboard.ts +++ b/src/mahoji/commands/leaderboard.ts @@ -324,20 +324,10 @@ async function clLb( ) { const { resolvedCl, items } = getCollectionItems(inputType, false, false, true); if (!items || items.length === 0) { - return "That's not a valid collection log category. Check +cl for all possible logs."; + return "That's not a valid collection log category. Check /cl for all possible logs."; } - const userEventOrders = await prisma.userEvent.findMany({ - where: { - type: 'CLCompletion', - collection_log_name: resolvedCl.toLowerCase() - }, - orderBy: { - date: 'asc' - } - }); - - const users = await fetchCLLeaderboard({ ironmenOnly, items, resultLimit: 200, userEvents: userEventOrders }); + const { users } = await fetchCLLeaderboard({ ironmenOnly, items, resultLimit: 200, clName: resolvedCl }); inputType = toTitleCase(inputType.toLowerCase()); return doMenuWrapper({ diff --git a/src/mahoji/commands/mine.ts b/src/mahoji/commands/mine.ts index 8de09264f4..7f8ab8a624 100644 --- a/src/mahoji/commands/mine.ts +++ b/src/mahoji/commands/mine.ts @@ -91,7 +91,7 @@ export const mineCommand: OSBMahojiCommand = { return `To mine ${ore.name}, you need ${formatSkillRequirements(sinsOfTheFatherSkillRequirements)}.`; } if (user.QP < 125) { - return `To mine ${ore.name}, you need atleast 125 Quest Points.`; + return `To mine ${ore.name}, you need at least 125 Quest Points.`; } } diff --git a/src/mahoji/commands/mix.ts b/src/mahoji/commands/mix.ts index b16a74d103..85db808a86 100644 --- a/src/mahoji/commands/mix.ts +++ b/src/mahoji/commands/mix.ts @@ -72,7 +72,7 @@ export const mixCommand: OSBMahojiCommand = { } if (mixableItem.qpRequired && user.QP < mixableItem.qpRequired) { - return `You need atleast **${mixableItem.qpRequired}** QP to make ${mixableItem.item.name}.`; + return `You need at least **${mixableItem.qpRequired}** QP to make ${mixableItem.item.name}.`; } const requiredItems = new Bank(mixableItem.inputItems); diff --git a/src/mahoji/commands/offer.ts b/src/mahoji/commands/offer.ts index dd52db93ae..08401b2474 100644 --- a/src/mahoji/commands/offer.ts +++ b/src/mahoji/commands/offer.ts @@ -208,10 +208,10 @@ export const offerCommand: OSBMahojiCommand = { const specialBone = specialBones.find(bone => stringMatches(bone.item.name, options.name)); if (specialBone) { if (user.QP < 8) { - return 'You need atleast 8 QP to offer long/curved bones for XP.'; + return 'You need at least 8 QP to offer long/curved bones for XP.'; } if (user.skillLevel(SkillsEnum.Construction) < 30) { - return 'You need atleast level 30 Construction to offer long/curved bones for XP.'; + return 'You need at least level 30 Construction to offer long/curved bones for XP.'; } const amountHas = userBank.amount(specialBone.item.id); if (!quantity) quantity = Math.max(amountHas, 1); diff --git a/src/mahoji/commands/rp.ts b/src/mahoji/commands/rp.ts index b6499664b8..ee0f40f6ce 100644 --- a/src/mahoji/commands/rp.ts +++ b/src/mahoji/commands/rp.ts @@ -11,8 +11,7 @@ import { Bank } from 'oldschooljs'; import type { Item } from 'oldschooljs/dist/meta/types'; import { ADMIN_IDS, OWNER_IDS, SupportServer, production } from '../../config'; -import { analyticsTick } from '../../lib/analytics'; -import { BitField, Channel } from '../../lib/constants'; +import { BitField, Channel, globalConfig } from '../../lib/constants'; import { allCollectionLogsFlat } from '../../lib/data/Collections'; import type { GearSetupType } from '../../lib/gear/types'; import { GrandExchange } from '../../lib/grandExchange'; @@ -21,14 +20,16 @@ import { unEquipAllCommand } from '../../lib/minions/functions/unequipAllCommand import { unequipPet } from '../../lib/minions/functions/unequipPet'; import { premiumPatronTime } from '../../lib/premiumPatronTime'; +import { runRolesTask } from '../../lib/rolesTask'; import { TeamLoot } from '../../lib/simulation/TeamLoot'; import { SkillsEnum } from '../../lib/skilling/types'; import type { ItemBank } from '../../lib/types'; -import { dateFm, isValidDiscordSnowflake, returnStringOrFile } from '../../lib/util'; +import { dateFm, isValidDiscordSnowflake } from '../../lib/util'; import getOSItem from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction } from '../../lib/util/interactionReply'; import itemIsTradeable from '../../lib/util/itemIsTradeable'; +import { logError } from '../../lib/util/logError'; import { makeBankImage } from '../../lib/util/makeBankImage'; import { migrateUser } from '../../lib/util/migrateUser'; import { parseBank } from '../../lib/util/parseStringBank'; @@ -44,7 +45,14 @@ import { sellPriceOfItem } from './sell'; const itemFilters = [ { name: 'Tradeable', - filter: (item: Item) => itemIsTradeable(item.id, true) + filter: (item: Item) => itemIsTradeable(item.id, true), + run: async () => { + const isValid = await GrandExchange.extensiveVerification(); + if (isValid) { + return 'No issues found.'; + } + return 'Something was invalid. Check logs!'; + } } ]; @@ -138,10 +146,39 @@ async function usernameSync() { function isProtectedAccount(user: MUser) { const botAccounts = ['303730326692429825', '729244028989603850', '969542224058654790']; if ([...ADMIN_IDS, ...OWNER_IDS, ...botAccounts].includes(user.id)) return true; - if ([BitField.isModerator, BitField.isContributor].some(bf => user.bitfield.includes(bf))) return true; + if ([BitField.isModerator].some(bf => user.bitfield.includes(bf))) return true; return false; } +const actions = [ + { + name: 'validate_ge', + allowed: (user: MUser) => ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id), + run: async () => { + const isValid = await GrandExchange.extensiveVerification(); + if (isValid) { + return 'No issues found.'; + } + return 'Something was invalid. Check logs!'; + } + }, + { + name: 'sync_roles', + allowed: (user: MUser) => + ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id) || user.bitfield.includes(BitField.isModerator), + run: async () => { + return runRolesTask(!globalConfig.isProduction); + } + }, + { + name: 'sync_usernames', + allowed: (user: MUser) => ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id), + run: async () => { + return usernameSync(); + } + } +]; + export const rpCommand: OSBMahojiCommand = { name: 'rp', description: 'Admin tools second set', @@ -150,45 +187,13 @@ export const rpCommand: OSBMahojiCommand = { { type: ApplicationCommandOptionType.SubcommandGroup, name: 'action', - description: 'Action tools', - options: [ - { - type: ApplicationCommandOptionType.Subcommand, - name: 'validate_ge', - description: 'Validate the g.e.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - 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: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'analytics_tick', - description: 'analyticsTick.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'networth_sync', - description: 'networth_sync.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'redis_sync', - description: 'redis sync.', - options: [] - } - ] + description: 'Actions', + options: actions.map(a => ({ + type: ApplicationCommandOptionType.Subcommand, + name: a.name, + description: a.name, + options: [] + })) }, { type: ApplicationCommandOptionType.SubcommandGroup, @@ -520,14 +525,7 @@ export const rpCommand: OSBMahojiCommand = { max_total?: { user: MahojiUserOption; type: UserEventType; message_id: string }; max?: { user: MahojiUserOption; type: UserEventType; skill: xp_gains_skill_enum; message_id: string }; }; - action?: { - validate_ge?: {}; - patreon_reset?: {}; - view_all_items?: {}; - analytics_tick?: {}; - networth_sync?: {}; - redis_sync?: {}; - }; + action?: any; player?: { viewbank?: { user: MahojiUserOption; json?: boolean }; add_patron_time?: { user: MahojiUserOption; tier: number; time: string }; @@ -564,11 +562,8 @@ export const rpCommand: OSBMahojiCommand = { const isOwner = OWNER_IDS.includes(userID.toString()); const isAdmin = ADMIN_IDS.includes(userID); const isMod = isOwner || isAdmin || adminUser.bitfield.includes(BitField.isModerator); - const isTrusted = [BitField.IsWikiContributor, BitField.isContributor].some(bit => - adminUser.bitfield.includes(bit) - ); if (!guildID || (production && guildID.toString() !== SupportServer)) return randArrItem(gifs); - if (!isAdmin && !isMod && !isTrusted) return randArrItem(gifs); + if (!isAdmin && !isMod) return randArrItem(gifs); if (options.user_event) { const messageId = @@ -623,52 +618,19 @@ Date: ${dateFm(date)}`; if (!isMod) return randArrItem(gifs); - if (options.action?.validate_ge) { - const isValid = await GrandExchange.extensiveVerification(); - if (isValid) { - return 'No issues found.'; - } - return 'Something was invalid. Check logs!'; - } - if (options.action?.analytics_tick) { - await analyticsTick(); - return 'Finished.'; - } - if (options.action?.redis_sync) { - const result = await usernameSync(); - return result; - } - if (options.action?.networth_sync) { - const users = await prisma.user.findMany({ - where: { - GP: { - gt: 10_000_000_000 + if (options.action) { + for (const action of actions) { + if (options.action[action.name]) { + if (!action.allowed(adminUser)) return randArrItem(gifs); + try { + const result = await action.run(); + return result; + } catch (err) { + logError(err); + return 'An error occurred.'; } - }, - take: 20, - orderBy: { - GP: 'desc' - }, - select: { - id: true } - }); - for (const { id } of users) { - const user = await mUserFetch(id); - await user.update({ - cached_networth_value: (await user.calculateNetWorth()).value - }); } - return 'Done.'; - } - 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.player?.set_buy_date) { diff --git a/src/mahoji/commands/sacrifice.ts b/src/mahoji/commands/sacrifice.ts index 31916e619a..1b5019e57e 100644 --- a/src/mahoji/commands/sacrifice.ts +++ b/src/mahoji/commands/sacrifice.ts @@ -8,7 +8,7 @@ import { Emoji, Events } from '../../lib/constants'; import { cats } from '../../lib/growablePets'; import minionIcons from '../../lib/minions/data/minionIcons'; import type { ItemBank } from '../../lib/types'; -import { toKMB } from '../../lib/util'; +import { toKMB, truncateString } from '../../lib/util'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction } from '../../lib/util/interactionReply'; import { parseBank } from '../../lib/util/parseStringBank'; @@ -156,7 +156,7 @@ export const sacrificeCommand: OSBMahojiCommand = { await handleMahojiConfirmation( interaction, - `${user}, are you sure you want to sacrifice ${bankToSac}? This will add ${totalPrice.toLocaleString()} (${toKMB( + `${user}, are you sure you want to sacrifice ${truncateString(bankToSac.toString(), 15000)}? This will add ${totalPrice.toLocaleString()} (${toKMB( totalPrice )}) to your sacrificed amount.` ); diff --git a/src/mahoji/commands/simulate.ts b/src/mahoji/commands/simulate.ts index a55a6f43c3..7ab6de1696 100644 --- a/src/mahoji/commands/simulate.ts +++ b/src/mahoji/commands/simulate.ts @@ -57,7 +57,10 @@ function simulateColosseumRuns(samples = 100) { hasBF: false, hasClaws: true, hasSGS: true, - hasTorture: true + hasTorture: true, + scytheCharges: 300, + venatorBowCharges: 50, + bloodFuryCharges: 0 }); totalDuration += result.realDuration; kcBank.add(result.addedWaveKCBank); diff --git a/src/mahoji/commands/smelt.ts b/src/mahoji/commands/smelt.ts index ca8bded912..d099c2d281 100644 --- a/src/mahoji/commands/smelt.ts +++ b/src/mahoji/commands/smelt.ts @@ -145,7 +145,7 @@ export const smeltingCommand: OSBMahojiCommand = { coinsToRemove = Math.floor(gpPerHour * (duration / Time.Hour)); const gp = user.GP; if (gp < coinsToRemove) { - return `You need atleast ${coinsToRemove} GP to work at the Blast Furnace.`; + return `You need at least ${coinsToRemove} GP to work at the Blast Furnace.`; } cost.add('Coins', coinsToRemove); diff --git a/src/mahoji/commands/steal.ts b/src/mahoji/commands/steal.ts index 5ba6d227d1..c1248ed3df 100644 --- a/src/mahoji/commands/steal.ts +++ b/src/mahoji/commands/steal.ts @@ -70,7 +70,7 @@ export const stealCommand: OSBMahojiCommand = { } if (stealable.qpRequired && user.QP < stealable.qpRequired) { - return `You need atleast **${stealable.qpRequired}** QP to ${ + return `You need at least **${stealable.qpRequired}** QP to ${ stealable.type === 'pickpockable' ? 'pickpocket' : 'steal from' } a ${stealable.name}.`; } diff --git a/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts b/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts index d8e69518b2..b21649e697 100644 --- a/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts +++ b/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts @@ -8,7 +8,7 @@ import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; export async function aerialFishingCommand(user: MUser, channelID: string) { if (user.skillLevel(SkillsEnum.Fishing) < 43 || user.skillLevel(SkillsEnum.Hunter) < 35) { - return 'You need atleast level 35 Hunter and 43 Fishing to do Aerial fishing.'; + return 'You need at least level 35 Hunter and 43 Fishing to do Aerial fishing.'; } const timePerFish = randomVariation(2, 7.5) * Time.Second; diff --git a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts index 6d2d7183dc..f6e99e8ad3 100644 --- a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts +++ b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts @@ -17,7 +17,7 @@ interface AutoslayLink { // Name and Monster must be specified if either is. efficientName?: string; efficientMonster?: number; - efficientMethod?: PvMMethod; + efficientMethod?: PvMMethod | PvMMethod[]; slayerMasters?: SlayerMasterEnum[]; } @@ -147,7 +147,7 @@ const AutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.SmokeDevil.id, efficientName: Monsters.SmokeDevil.name, efficientMonster: Monsters.SmokeDevil.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.DarkBeast.id, @@ -216,13 +216,13 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.AbyssalDemon.id, efficientName: Monsters.AbyssalDemon.name, efficientMonster: Monsters.AbyssalDemon.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.Ankou.id, efficientName: Monsters.Ankou.name, efficientMonster: Monsters.Ankou.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.BlackDemon.id, @@ -240,7 +240,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.Bloodveld.id, efficientName: Monsters.Bloodveld.name, efficientMonster: Monsters.Bloodveld.id, - efficientMethod: 'barrage' + efficientMethod: 'none' }, { monsterID: Monsters.ChaosDruid.id, @@ -264,7 +264,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.DustDevil.id, efficientName: Monsters.DustDevil.name, efficientMonster: Monsters.DustDevil.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.ElderChaosDruid.id, @@ -318,7 +318,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.Jelly.id, efficientName: Monsters.Jelly.name, efficientMonster: Monsters.Jelly.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.LesserDemon.id, @@ -348,7 +348,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.GreaterNechryael.id, efficientName: Monsters.GreaterNechryael.name, efficientMonster: Monsters.GreaterNechryael.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.RevenantImp.id, @@ -485,7 +485,7 @@ export async function autoSlayCommand({ name: ehpMonster.efficientName }; if (ehpMonster.efficientMethod) { - args.method = ehpMonster.efficientMethod; + args.method = ehpMonster.efficientMethod as unknown as CommandOptions; } runCommand({ commandName: 'k', diff --git a/src/mahoji/lib/abstracted_commands/bankBgCommand.ts b/src/mahoji/lib/abstracted_commands/bankBgCommand.ts index 4cae340826..9af5fa5346 100644 --- a/src/mahoji/lib/abstracted_commands/bankBgCommand.ts +++ b/src/mahoji/lib/abstracted_commands/bankBgCommand.ts @@ -36,7 +36,7 @@ export async function bankBgCommand(interaction: ChatInputCommandInteraction, us if (selectedImage.sacValueRequired) { const sac = Number(user.user.sacrificedValue); if (sac < selectedImage.sacValueRequired) { - return `You have to have sacrificed atleast ${toKMB( + return `You have to have sacrificed at least ${toKMB( selectedImage.sacValueRequired )} GP worth of items to use this background.`; } diff --git a/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts b/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts index 0e2a1853a1..49341819e6 100644 --- a/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts +++ b/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts @@ -40,7 +40,7 @@ export async function chompyHuntClaimCommand(user: MUser) { export async function chompyHuntCommand(user: MUser, channelID: string) { if (user.QP < 10) { - return 'You need atleast 10 QP to hunt Chompy birds.'; + return 'You need at least 10 QP to hunt Chompy birds.'; } const rangeGear = user.gear.range; diff --git a/src/mahoji/lib/abstracted_commands/coxCommand.ts b/src/mahoji/lib/abstracted_commands/coxCommand.ts index 366ed284e0..6eeaca3135 100644 --- a/src/mahoji/lib/abstracted_commands/coxCommand.ts +++ b/src/mahoji/lib/abstracted_commands/coxCommand.ts @@ -95,7 +95,7 @@ export async function coxCommand( if (isChallengeMode) { const normalKC = await getMinigameScore(user.id, 'raids'); if (normalKC < 200) { - return 'You need atleast 200 completions of the Chambers of Xeric before you can attempt Challenge Mode.'; + return 'You need at least 200 completions of the Chambers of Xeric before you can attempt Challenge Mode.'; } } if (user.minionIsBusy) { diff --git a/src/mahoji/lib/abstracted_commands/diceCommand.ts b/src/mahoji/lib/abstracted_commands/diceCommand.ts index b53ebbbe98..f8a4117615 100644 --- a/src/mahoji/lib/abstracted_commands/diceCommand.ts +++ b/src/mahoji/lib/abstracted_commands/diceCommand.ts @@ -29,7 +29,7 @@ export async function diceCommand(user: MUser, interaction: ChatInputCommandInte } if (amount < 1_000_000) { - return 'You have to dice atleast 1,000,000.'; + return 'You have to dice at least 1,000,000.'; } const gp = user.GP; diff --git a/src/mahoji/lib/abstracted_commands/driftNetCommand.ts b/src/mahoji/lib/abstracted_commands/driftNetCommand.ts index 9c24f5cde9..de2b715d50 100644 --- a/src/mahoji/lib/abstracted_commands/driftNetCommand.ts +++ b/src/mahoji/lib/abstracted_commands/driftNetCommand.ts @@ -21,7 +21,7 @@ export async function driftNetCommand( } if (user.skillLevel(SkillsEnum.Fishing) < 47 || user.skillLevel(SkillsEnum.Hunter) < 44) { - return 'You need atleast level 44 Hunter and 47 Fishing to do Drift net fishing.'; + return 'You need at least level 44 Hunter and 47 Fishing to do Drift net fishing.'; } if (!user.hasEquipped(['Graceful gloves', 'Graceful top', 'Graceful legs'])) { diff --git a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts index b30248334e..543a275fa5 100644 --- a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts +++ b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts @@ -86,11 +86,11 @@ function checkGear(user: MUser): string | undefined { } if (!user.owns(fightCavesCost)) { - return `JalYt, you need supplies to have a chance in the caves...come back with ${fightCavesCost}.`; + return `JalYt, you need supplies to have a chance in the caves... Come back with ${fightCavesCost}.`; } if (user.skillLevel('prayer') < 43) { - return 'JalYt, come back when you have atleast 43 Prayer, TzTok-Jad annihilate you without protection from gods.'; + return 'JalYt, come back when you have at least 43 Prayer, TzTok-Jad annihilate you without protection from gods.'; } } diff --git a/src/mahoji/lib/abstracted_commands/fishingTrawler.ts b/src/mahoji/lib/abstracted_commands/fishingTrawler.ts index 36e4ccb8fd..21657215d8 100644 --- a/src/mahoji/lib/abstracted_commands/fishingTrawler.ts +++ b/src/mahoji/lib/abstracted_commands/fishingTrawler.ts @@ -8,7 +8,7 @@ import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; export async function fishingTrawlerCommand(user: MUser, channelID: string) { if (user.skillLevel('fishing') < 15) { - return 'You need atleast level 15 Fishing to do the Fishing Trawler.'; + return 'You need at least level 15 Fishing to do the Fishing Trawler.'; } const tripsDone = await getMinigameScore(user.id, 'fishing_trawler'); diff --git a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts index 086ba8d13f..362c9a1d18 100644 --- a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts +++ b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts @@ -44,7 +44,7 @@ const corruptedRequirements = { export async function gauntletCommand(user: MUser, channelID: string, type: 'corrupted' | 'normal' = 'normal') { if (user.minionIsBusy) return `${user.minionName} is busy.`; if (user.QP < 200) { - return 'You need atleast 200 QP to do the Gauntlet.'; + return 'You need at least 200 QP to do the Gauntlet.'; } const readableName = `${toTitleCase(type)} Gauntlet`; const requiredSkills = type === 'corrupted' ? corruptedRequirements : standardRequirements; diff --git a/src/mahoji/lib/abstracted_commands/gearCommands.ts b/src/mahoji/lib/abstracted_commands/gearCommands.ts index 3cb1fb6c54..b3a98117ce 100644 --- a/src/mahoji/lib/abstracted_commands/gearCommands.ts +++ b/src/mahoji/lib/abstracted_commands/gearCommands.ts @@ -406,7 +406,7 @@ export async function gearViewCommand(user: MUser, input: string, text: boolean) }) .join('\n\n'); - const updatedContent = `${content}\n\nThese assume you have atleast 25 prayer for the protect item prayer.`; + const updatedContent = `${content}\n\nThese assume you have at least 25 prayer for the protect item prayer.`; return { content: updatedContent }; } diff --git a/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts b/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts index 63bb5cfa19..2576fa30ff 100644 --- a/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts +++ b/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts @@ -172,7 +172,7 @@ export async function giantsFoundryStartCommand( } if (userSmithingLevel < alloy.level) { - return `${user.minionName} needs atleast level ${alloy.level} Smithing to user ${alloy.name} alloy in the Giants' Foundry.`; + return `${user.minionName} needs at least level ${alloy.level} Smithing to user ${alloy.name} alloy in the Giants' Foundry.`; } // If they have the entire Smiths' Uniform, give an extra 15% speed bonus diff --git a/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts b/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts index 369f9f0a56..f78cc30f8e 100644 --- a/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts +++ b/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts @@ -18,7 +18,7 @@ export async function gnomeRestaurantCommand(user: MUser, channelID: string) { const itemsToRemove = new Bank(); const gp = user.GP; if (gp < 5000) { - return 'You need atleast 5k GP to work at the Gnome Restaurant.'; + return 'You need at least 5k GP to work at the Gnome Restaurant.'; } itemsToRemove.add('Coins', 5000); diff --git a/src/mahoji/lib/abstracted_commands/infernoCommand.ts b/src/mahoji/lib/abstracted_commands/infernoCommand.ts index 56f2e1f54f..080b9ff36d 100644 --- a/src/mahoji/lib/abstracted_commands/infernoCommand.ts +++ b/src/mahoji/lib/abstracted_commands/infernoCommand.ts @@ -249,7 +249,7 @@ async function infernoRun({ const dartIndex = blowpipeDarts.indexOf(dartItem); const percent = dartIndex >= 3 ? dartIndex * 0.9 : -(4 * (4 - dartIndex)); if (dartIndex < 5) { - return 'Your darts are simply too weak, to work in the Inferno!'; + return 'Your darts are simply too weak to work in the Inferno!'; } duration.add(true, -percent, `${dartItem.name} in blowpipe`); @@ -516,7 +516,7 @@ export async function infernoStartCommand(user: MUser, channelID: string): Comma { name: 'image.jpg', attachment: await newChatHeadImage({ - content: "You're on your own now JalYt, you face certain death... prepare to fight for your life.", + content: "You're on your own now JalYt, you face certain death... Prepare to fight for your life.", head: 'ketKeh' }) } diff --git a/src/mahoji/lib/abstracted_commands/ironmanCommand.ts b/src/mahoji/lib/abstracted_commands/ironmanCommand.ts index a2d779c4c9..a1013f73b0 100644 --- a/src/mahoji/lib/abstracted_commands/ironmanCommand.ts +++ b/src/mahoji/lib/abstracted_commands/ironmanCommand.ts @@ -127,13 +127,11 @@ After becoming an ironman: BitField.IsPatronTier4, BitField.IsPatronTier5, BitField.isModerator, - BitField.isContributor, BitField.BypassAgeRestriction, BitField.HasPermanentEventBackgrounds, BitField.HasPermanentTierOne, BitField.DisabledRandomEvents, BitField.AlwaysSmallBank, - BitField.IsWikiContributor, BitField.IsPatronTier6 ]; diff --git a/src/mahoji/lib/abstracted_commands/lampCommand.ts b/src/mahoji/lib/abstracted_commands/lampCommand.ts index 2a5d9bb0b3..db678e76fb 100644 --- a/src/mahoji/lib/abstracted_commands/lampCommand.ts +++ b/src/mahoji/lib/abstracted_commands/lampCommand.ts @@ -84,13 +84,12 @@ export const XPLamps: IXPLamp[] = [ minimumLevel: 1, allowedSkills: [SkillsEnum.Magic] }, - /* Needs OSJS Update { itemID: 28_820, amount: 5000, name: 'Antique lamp (defender of varrock)', minimumLevel: 1 - },*/ + }, { itemID: itemID('Antique lamp (easy ca)'), amount: 5000, @@ -149,6 +148,13 @@ export const Lampables: IXPObject[] = [ skills[skill] = data.user.skillLevel(skill) * ([ + SkillsEnum.Attack, + SkillsEnum.Strength, + SkillsEnum.Defence, + SkillsEnum.Magic, + SkillsEnum.Ranged, + SkillsEnum.Hitpoints, + SkillsEnum.Prayer, SkillsEnum.Mining, SkillsEnum.Woodcutting, SkillsEnum.Herblore, diff --git a/src/mahoji/lib/abstracted_commands/lmsCommand.ts b/src/mahoji/lib/abstracted_commands/lmsCommand.ts index e7460c0fb4..056bea5212 100644 --- a/src/mahoji/lib/abstracted_commands/lmsCommand.ts +++ b/src/mahoji/lib/abstracted_commands/lmsCommand.ts @@ -53,7 +53,7 @@ export async function lmsCommand( return `You don't have enough points. ${quantity}x ${itemToBuy.item.name} costs ${cost}, but you have ${stats.points}.`; } if (itemToBuy.wins && stats.gamesWon < itemToBuy.wins) { - return `You are not worthy! You need to have won atleast ${itemToBuy.wins} games to buy the ${itemToBuy.item.name}.`; + return `You are not worthy! You need to have won at least ${itemToBuy.wins} games to buy the ${itemToBuy.item.name}.`; } const loot = new Bank().add(itemToBuy.item.id, quantity * (itemToBuy.quantity ?? 1)); await handleMahojiConfirmation(interaction, `Are you sure you want to spend ${cost} points on buying ${loot}?`); diff --git a/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts b/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts index 1ee8cb5719..3c71531837 100644 --- a/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts +++ b/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts @@ -113,7 +113,7 @@ ${mageTrainingArenaBuyables .map(i => `${i.item.name} - ${i.cost} pts - ${formatDuration((i.cost / pizazzPointsPerHour) * (Time.Minute * 60))}`) .join('\n')} -Hint: Magic Training Arena is combined into 1 room, and 1 set of points - rewards take approximately the same amount of time to get. To get started use **/minigames mage_training_arena train**. You can buy rewards using **/minigames mage_training_arena buy**.`; +Hint: Magic Training Arena is combined into 1 room, and 1 set of points - rewards take approximately the same amount of time to get. To get started use **/minigames mage_training_arena start**. You can buy rewards using **/minigames mage_training_arena buy**.`; } export async function mageTrainingArenaStartCommand(user: MUser, channelID: string): CommandResponse { diff --git a/src/mahoji/lib/abstracted_commands/minionKill.ts b/src/mahoji/lib/abstracted_commands/minionKill.ts index 94bd7ad621..f729fee9a7 100644 --- a/src/mahoji/lib/abstracted_commands/minionKill.ts +++ b/src/mahoji/lib/abstracted_commands/minionKill.ts @@ -48,7 +48,7 @@ import type { Consumable } from '../../../lib/minions/types'; import { calcPOHBoosts } from '../../../lib/poh'; import { SkillsEnum } from '../../../lib/skilling/types'; import { SlayerTaskUnlocksEnum } from '../../../lib/slayer/slayerUnlocks'; -import { determineBoostChoice, getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; +import { determineCombatBoosts, getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; import { maxOffenceStats } from '../../../lib/structures/Gear'; import type { Peak } from '../../../lib/tickers'; import type { MonsterActivityTaskOptions } from '../../../lib/types/minions'; @@ -134,7 +134,7 @@ export async function minionKillCommand( channelID: string, name: string, quantity: number | undefined, - method: PvMMethod | undefined, + method: PvMMethod | PvMMethod[] | undefined, wilderness: boolean | undefined, solo: boolean | undefined ): Promise { @@ -206,19 +206,18 @@ export async function minionKillCommand( } } - // Add jelly check as can barrage in wilderness + // Add check for burstable monsters in wilderness const jelly = monster.id === Monsters.Jelly.id; - const bloodveld = monster.id === Monsters.Bloodveld.id; + const wildyBurst = jelly && isInWilderness; - const wildyBurst = (jelly || bloodveld) && isInWilderness; - - // Set chosen boost based on priority: + // determines what pvm methods the user can use const myCBOpts = user.combatOptions; - const boostChoice = determineBoostChoice({ + const methods = [method] as PvMMethod[]; + const combatMethods = determineCombatBoosts({ cbOpts: myCBOpts as CombatOptionsEnum[], user, monster, - method, + methods, isOnTask, wildyBurst }); @@ -239,7 +238,7 @@ export async function minionKillCommand( const [, osjsMon, attackStyles] = resolveAttackStyles(user, { monsterID: monster.id, - boostMethod: boostChoice + boostMethod: combatMethods }); const [newTime, skillBoostMsg] = applySkillBoost(user, timeToFinish, attackStyles); @@ -457,21 +456,25 @@ export async function minionKillCommand( } // Check for stats - if (boostChoice === 'barrage' && user.skillLevel(SkillsEnum.Magic) < 94) { + if (combatMethods.includes('barrage') && user.skillLevel(SkillsEnum.Magic) < 94) { return `You need 94 Magic to use Ice Barrage. You have ${user.skillLevel(SkillsEnum.Magic)}`; } - if (boostChoice === 'burst' && user.skillLevel(SkillsEnum.Magic) < 70) { + if (combatMethods.includes('burst') && user.skillLevel(SkillsEnum.Magic) < 70) { return `You need 70 Magic to use Ice Burst. You have ${user.skillLevel(SkillsEnum.Magic)}`; } - if (boostChoice === 'chinning' && user.skillLevel(SkillsEnum.Ranged) < 65) { + if (combatMethods.includes('chinning') && user.skillLevel(SkillsEnum.Ranged) < 65) { return `You need 65 Ranged to use Chinning method. You have ${user.skillLevel(SkillsEnum.Ranged)}`; } - // Wildy Monster checks - if (isInWilderness === true && boostChoice === 'cannon') { + // Wildy monster cannon checks + if (isInWilderness === true && combatMethods.includes('cannon')) { if (monster.id === Monsters.HillGiant.id || monster.id === Monsters.MossGiant.id) { usingCannon = isInWilderness; } + if (monster.id === Monsters.Spider.id || Monsters.Scorpion.id) { + usingCannon = isInWilderness; + cannonMulti = isInWilderness; + } if (monster.wildySlayerCave) { usingCannon = isInWilderness; cannonMulti = isInWilderness; @@ -482,8 +485,9 @@ export async function minionKillCommand( } } + // Burst/barrage check with wilderness conditions if ((method === 'burst' || method === 'barrage') && !monster?.canBarrage) { - if (jelly || bloodveld) { + if (jelly) { if (!isInWilderness) { return `${monster.name} can only be barraged or burst in the wilderness.`; } @@ -496,14 +500,18 @@ export async function minionKillCommand( } } - if (boostChoice === 'barrage' && attackStyles.includes(SkillsEnum.Magic) && (monster?.canBarrage || wildyBurst)) { + if ( + combatMethods.includes('barrage') && + attackStyles.includes(SkillsEnum.Magic) && + (monster?.canBarrage || wildyBurst) + ) { consumableCosts.push(iceBarrageConsumables); calculateVirtusBoost(); timeToFinish = reduceNumByPercent(timeToFinish, boostIceBarrage + virtusBoost); boosts.push(`${boostIceBarrage + virtusBoost}% for Ice Barrage${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBarrage; } else if ( - boostChoice === 'burst' && + combatMethods.includes('burst') && attackStyles.includes(SkillsEnum.Magic) && (monster?.canBarrage || wildyBurst) ) { @@ -512,13 +520,14 @@ export async function minionKillCommand( timeToFinish = reduceNumByPercent(timeToFinish, boostIceBurst + virtusBoost); boosts.push(`${boostIceBurst + virtusBoost}% for Ice Burst${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBurst; - } else if ((boostChoice === 'cannon' && hasCannon && monster?.cannonMulti) || cannonMulti) { + } + if ((combatMethods.includes('cannon') && hasCannon && monster?.cannonMulti) || cannonMulti) { usingCannon = true; cannonMulti = true; consumableCosts.push(cannonMultiConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannonMulti); boosts.push(`${boostCannonMulti}% for Cannon in multi`); - } else if ((boostChoice === 'cannon' && hasCannon && monster?.canCannon) || usingCannon) { + } else if ((combatMethods.includes('cannon') && hasCannon && monster?.canCannon) || usingCannon) { usingCannon = true; consumableCosts.push(cannonSingleConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannon); diff --git a/src/mahoji/lib/abstracted_commands/nightmareCommand.ts b/src/mahoji/lib/abstracted_commands/nightmareCommand.ts index 759dd7800c..170602cfdb 100644 --- a/src/mahoji/lib/abstracted_commands/nightmareCommand.ts +++ b/src/mahoji/lib/abstracted_commands/nightmareCommand.ts @@ -89,7 +89,7 @@ async function checkReqs(user: MUser, monster: KillableMonster, isPhosani: boole return `${user.usernameOrMention} doesn't meet the requirements: ${requirements[1]}.`; } if ((await user.getKC(NightmareMonster.id)) < 50) { - return "You need to have killed The Nightmare atleast 50 times before you can face the Phosani's Nightmare."; + return "You need to have killed The Nightmare at least 50 times before you can face the Phosani's Nightmare."; } } } @@ -100,13 +100,13 @@ function perUserCost(user: MUser, quantity: number, isPhosani: boolean, hasShado const sangCharges = sangChargesPerKc * quantity; if (isPhosani) { if (hasShadow && user.user.tum_shadow_charges < tumCharges) { - return `You need atleast ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' )}`; } else if (hasSang && user.user.sang_charges < sangCharges) { - return `You need atleast ${sangCharges} Sanguinesti staff charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${sangCharges} Sanguinesti staff charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' diff --git a/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts b/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts index 41a816b15d..c4142eb55d 100644 --- a/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts +++ b/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts @@ -68,7 +68,7 @@ export async function puroPuroStartCommand( } if (!impToHunt) return 'Error selecting impling, please try again.'; if (hunterLevel < impToHunt.hunterLevel) - return `${user.minionName} needs atleast level ${impToHunt.hunterLevel} hunter to hunt ${impToHunt.name} in Puro-Puro.`; + return `${user.minionName} needs at least level ${impToHunt.hunterLevel} hunter to hunt ${impToHunt.name} in Puro-Puro.`; if (!darkLure || (darkLure && !impToHunt.spell)) darkLure = false; if (darkLure) { if (user.QP < 9) return 'To use Dark Lure, you need 9 QP.'; diff --git a/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts b/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts index 8825d580a5..9c51f0e8ab 100644 --- a/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts +++ b/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts @@ -15,7 +15,7 @@ export async function pyramidPlunderCommand(user: MUser, channelID: string) { const thievingLevel = skills.thieving; const minLevel = plunderRooms[0].thievingLevel; if (thievingLevel < minLevel) { - return `You need atleast level ${minLevel} Thieving to do the Pyramid Plunder.`; + return `You need at least level ${minLevel} Thieving to do the Pyramid Plunder.`; } const completableRooms = plunderRooms.filter(room => thievingLevel >= room.thievingLevel); diff --git a/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts b/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts index e1bc9defe6..46b2c4fc29 100644 --- a/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts +++ b/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts @@ -14,11 +14,11 @@ export async function sepulchreCommand(user: MUser, channelID: string) { const thievingLevel = skills.thieving; const minLevel = sepulchreFloors[0].agilityLevel; if (agilityLevel < minLevel) { - return `You need atleast level ${minLevel} Agility to do the Hallowed Sepulchre.`; + return `You need at least level ${minLevel} Agility to do the Hallowed Sepulchre.`; } if (thievingLevel < 66) { - return 'You need atleast level 66 Thieving to do the Hallowed Sepulchre.'; + return 'You need at least level 66 Thieving to do the Hallowed Sepulchre.'; } if (!userHasGracefulEquipped(user)) { diff --git a/src/mahoji/lib/abstracted_commands/tobCommand.ts b/src/mahoji/lib/abstracted_commands/tobCommand.ts index c8f9d1930b..5b6f1b9e06 100644 --- a/src/mahoji/lib/abstracted_commands/tobCommand.ts +++ b/src/mahoji/lib/abstracted_commands/tobCommand.ts @@ -180,11 +180,11 @@ async function checkTOBUser( } const dartsNeeded = 150 * quantity; if (blowpipeData.dartQuantity < dartsNeeded) { - return [true, `${user.usernameOrMention}, you need atleast ${dartsNeeded} darts in your blowpipe.`]; + return [true, `${user.usernameOrMention}, you need at least ${dartsNeeded} darts in your blowpipe.`]; } const scalesNeeded = 1000 * quantity; if (blowpipeData.scales < scalesNeeded) { - return [true, `${user.usernameOrMention}, you need atleast ${scalesNeeded} scales in your blowpipe.`]; + return [true, `${user.usernameOrMention}, you need at least ${scalesNeeded} scales in your blowpipe.`]; } const dartIndex = blowpipeDarts.indexOf(getOSItem(blowpipeData.dartID)); if (dartIndex < 5) { @@ -209,7 +209,7 @@ async function checkTOBUser( if (rangeGear.ammo!.quantity < arrowsRequired) { return [ true, - `${user.usernameOrMention}, you need atleast ${arrowsRequired} arrows equipped in your range setup.` + `${user.usernameOrMention}, you need at least ${arrowsRequired} arrows equipped in your range setup.` ]; } @@ -217,7 +217,7 @@ async function checkTOBUser( const kc = await getMinigameScore(user.id, 'tob'); if (kc < 250) { - return [true, `${user.usernameOrMention} needs atleast 250 Theatre of Blood KC before doing Hard mode.`]; + return [true, `${user.usernameOrMention} needs at least 250 Theatre of Blood KC before doing Hard mode.`]; } if (!meleeGear.hasEquipped('Infernal cape')) { return [true, `${user.usernameOrMention} needs an Infernal cape to do Hard mode.`]; @@ -227,7 +227,7 @@ async function checkTOBUser( if (teamSize === 2) { const kc = await getMinigameScore(user.id, isHardMode ? 'tob_hard' : 'tob'); if (kc < 150) { - return [true, `${user.usernameOrMention} needs atleast 150 KC before doing duo's.`]; + return [true, `${user.usernameOrMention} needs at least 150 KC before doing duo's.`]; } } @@ -306,7 +306,7 @@ export async function tobStartCommand( if (isHardMode) { const normalKC = await getMinigameScore(user.id, 'tob'); if (normalKC < 250) { - return 'You need atleast 250 completions of the Theatre of Blood before you can attempt Hard Mode.'; + return 'You need at least 250 completions of the Theatre of Blood before you can attempt Hard Mode.'; } } if (user.minionIsBusy) { diff --git a/src/mahoji/lib/abstracted_commands/trekCommand.ts b/src/mahoji/lib/abstracted_commands/trekCommand.ts index 12f05f54dd..25b62cfcdc 100644 --- a/src/mahoji/lib/abstracted_commands/trekCommand.ts +++ b/src/mahoji/lib/abstracted_commands/trekCommand.ts @@ -53,7 +53,7 @@ export async function trekCommand(user: MUser, channelID: string, difficulty: st if (!meetsRequirements) { return `You don't have the requirements to do ${tier.difficulty} treks! Your ${readableStatName( unmetKey! - )} stat in your ${setup} setup is ${has}, but you need atleast ${ + )} stat in your ${setup} setup is ${has}, but you need at least ${ tier.minimumGearRequirements[setup]?.[unmetKey!] }.`; } @@ -62,7 +62,7 @@ export async function trekCommand(user: MUser, channelID: string, difficulty: st } if (qp < 30) { - return 'You need atleast level 30 QP to do Temple Trekking.'; + return 'You need at least level 30 QP to do Temple Trekking.'; } if (minLevel !== undefined && user.combatLevel < minLevel) { diff --git a/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts b/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts index 8ddb48a360..6abdcf379d 100644 --- a/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts +++ b/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts @@ -234,6 +234,6 @@ export async function volcanicMineStatsCommand(user: MUser) { const currentUserPoints = user.user.volcanic_mine_points; const kc = await getMinigameScore(user.id, 'volcanic_mine'); - return `You have ${currentUserPoints.toLocaleString()} Volanic Mine points points. + return `You have ${currentUserPoints.toLocaleString()} Volcanic Mine points. You have completed ${kc} games of Volcanic Mine.`; } diff --git a/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts b/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts index 46a0472f69..fe12eddfff 100644 --- a/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts +++ b/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts @@ -82,7 +82,7 @@ async function cyclopsCommand(user: MUser, channelID: string, quantity: number | // Check if either 100 warrior guild tokens or attack cape (similar items in future) const amountTokens = userBank.amount('Warrior guild token'); if (!hasAttackCape && amountTokens < 100) { - return 'You need atleast 100 Warriors guild tokens to kill Cyclops.'; + return 'You need at least 100 Warriors guild tokens to kill Cyclops.'; } // If no quantity provided, set it to the max. if (!quantity) { @@ -107,7 +107,7 @@ async function cyclopsCommand(user: MUser, channelID: string, quantity: number | if (!hasAttackCape && amountTokens < tokensToSpend) { return `You don't have enough Warrior guild tokens to kill cyclopes for ${formatDuration( duration - )}, try a lower quantity. You need atleast ${Math.floor( + )}, try a lower quantity. You need at least ${Math.floor( (duration / Time.Minute) * 10 + 10 )}x Warrior guild tokens to kill ${quantity}x cyclopes.`; } @@ -144,7 +144,7 @@ export async function warriorsGuildCommand( const atkLvl = user.skillLevel('attack'); const strLvl = user.skillLevel('strength'); if (atkLvl + strLvl < 130 && atkLvl !== 99 && strLvl !== 99) { - return "To enter the Warrior's Guild, your Attack and Strength levels must add up to atleast 130, or you must have level 99 in either."; + return "To enter the Warrior's Guild, your Attack and Strength levels must add up to at least 130, or you must have level 99 in either."; } if (choice === 'cyclops') { diff --git a/src/mahoji/lib/events.ts b/src/mahoji/lib/events.ts index 05fd050229..556b4aeffc 100644 --- a/src/mahoji/lib/events.ts +++ b/src/mahoji/lib/events.ts @@ -1,6 +1,7 @@ import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { bulkUpdateCommands } from '@oldschoolgg/toolkit'; +import { ActivityType, bold, time } from 'discord.js'; import { Channel, META_CONSTANTS, globalConfig } from '../../lib/constants'; import { initCrons } from '../../lib/crons'; import { initTickers } from '../../lib/tickers'; @@ -9,6 +10,60 @@ import { mahojiClientSettingsFetch } from '../../lib/util/clientSettings'; import { sendToChannelID } from '../../lib/util/webhook'; import { CUSTOM_PRICE_CACHE } from '../commands/sell'; +export async function updateTestBotStatus(online = true) { + try { + if (globalConfig.isProduction) return; + const idMap: Record = { + '829398443821891634': '1265571664142270464', + '577488230539067403': '1265582554644217977', + '353484579840983042': '1265582554644217977', + '897549995446779964': '1265582743970910259', + '1158785741028081696': '1265583194108067925' + }; + const catChannelID = idMap[globalConfig.clientID]; + if (!catChannelID) return; + const cat = await globalClient.channels.fetch(catChannelID); + if (!cat || !cat.isTextBased() || cat.isDMBased()) { + console.log('Could not find status channel'); + return; + } + + const emoji = online ? '🟢' : '🔴'; + let text = ''; + if (online) { + text = `${emoji} ${globalClient.user.username} is ONLINE ${emoji} + +Turned on ${time(new Date(), 'R')}`; + text = bold(text); + } else { + text = `${emoji} ${globalClient.user.username} is offline ${emoji} + +Turned off ${time(new Date(), 'R')}`; + } + const message = await cat.messages + .fetch({ limit: 5 }) + .then(messages => messages.filter(m => m.author.id === globalClient.user!.id)) + .then(msg => msg.first()); + if (!message) { + await cat.send(text); + } else { + await message.edit(text); + } + if (online) { + await globalClient.user.setPresence({ + status: 'online', + activities: [ + { + name: `${emoji} ONLINE`, + type: ActivityType.Custom + } + ] + }); + } + } catch (err) { + console.error(err); + } +} export async function syncCustomPrices() { const clientData = await mahojiClientSettingsFetch({ custom_prices: true }); for (const [key, value] of Object.entries(clientData.custom_prices as ItemBank)) { @@ -17,26 +72,29 @@ export async function syncCustomPrices() { } export const onStartup = logWrapFn('onStartup', async () => { - globalClient.application.commands.fetch({ - guildId: globalConfig.isProduction ? undefined : globalConfig.testingServerID - }); - if (!globalConfig.isProduction) { - console.log('Syncing commands locally...'); - await bulkUpdateCommands({ - client: globalClient.mahojiClient, - commands: Array.from(globalClient.mahojiClient.commands.values()), - guildID: globalConfig.testingServerID - }); - } + const syncTestBotCommands = globalConfig.isProduction + ? null + : bulkUpdateCommands({ + client: globalClient.mahojiClient, + commands: Array.from(globalClient.mahojiClient.commands.values()), + guildID: globalConfig.testingServerID + }); initCrons(); initTickers(); - if (globalConfig.isProduction) { - sendToChannelID(Channel.GeneralChannel, { - content: `I have just turned on! + const sendStartupMessage = globalConfig.isProduction + ? sendToChannelID(Channel.GeneralChannel, { + content: `I have just turned on!\n\n${META_CONSTANTS.RENDERED_STR}` + }).catch(console.error) + : null; -${META_CONSTANTS.RENDERED_STR}` - }).catch(console.error); - } + await Promise.all([ + globalClient.application.commands.fetch({ + guildId: globalConfig.isProduction ? undefined : globalConfig.testingServerID + }), + updateTestBotStatus(), + sendStartupMessage, + syncTestBotCommands + ]); }); diff --git a/src/mahoji/lib/exitHandler.ts b/src/mahoji/lib/exitHandler.ts new file mode 100644 index 0000000000..14324c629b --- /dev/null +++ b/src/mahoji/lib/exitHandler.ts @@ -0,0 +1,21 @@ +import { TimerManager } from '@sapphire/timer-manager'; + +import { updateTestBotStatus } from './events'; + +export async function exitCleanup() { + try { + globalClient.isShuttingDown = true; + console.log('Cleaning up and exiting...'); + TimerManager.destroy(); + await updateTestBotStatus(false); + await Promise.all([ + globalClient.destroy(), + prisma.$disconnect(), + redis.disconnect(), + roboChimpClient.$disconnect() + ]); + console.log('\nCleaned up and exited.'); + } catch (err) { + console.error(err); + } +} diff --git a/src/mahoji/lib/inhibitors.ts b/src/mahoji/lib/inhibitors.ts index 19da2ee254..2f3685e47f 100644 --- a/src/mahoji/lib/inhibitors.ts +++ b/src/mahoji/lib/inhibitors.ts @@ -144,8 +144,7 @@ const inhibitors: Inhibitor[] = [ // Allow contributors + moderators to use disabled channels in SupportServer const userBitfield = user.bitfield; - const isStaff = - userBitfield.includes(BitField.isModerator) || userBitfield.includes(BitField.isContributor); + const isStaff = userBitfield.includes(BitField.isModerator); if (guild.id === SupportServer && isStaff) { return false; } diff --git a/src/mahoji/lib/postCommand.ts b/src/mahoji/lib/postCommand.ts index cc1c53b674..eceb159855 100644 --- a/src/mahoji/lib/postCommand.ts +++ b/src/mahoji/lib/postCommand.ts @@ -3,6 +3,7 @@ import type { CommandOptions } from '@oldschoolgg/toolkit'; import { modifyBusyCounter } from '../../lib/busyCounterCache'; import { busyImmuneCommands, shouldTrackCommand } from '../../lib/constants'; +import { TimerManager } from '@sapphire/timer-manager'; import { makeCommandUsage } from '../../lib/util/commandUsage'; import { logError } from '../../lib/util/logError'; import type { AbstractCommand } from './inhibitors'; @@ -28,7 +29,7 @@ export async function postCommand({ continueDeltaMillis: number | null; }): Promise { if (!busyImmuneCommands.includes(abstractCommand.name)) { - setTimeout(() => modifyBusyCounter(userID, -1), 1000); + TimerManager.setTimeout(() => modifyBusyCounter(userID, -1), 1000); } if (shouldTrackCommand(abstractCommand, args)) { const commandUsage = makeCommandUsage({ diff --git a/src/mahoji/mahojiSettings.ts b/src/mahoji/mahojiSettings.ts index 6eb0c25293..e4df529d54 100644 --- a/src/mahoji/mahojiSettings.ts +++ b/src/mahoji/mahojiSettings.ts @@ -257,7 +257,7 @@ export function hasMonsterRequirements(user: MUser, monster: KillableMonster) { false, `You don't have the requirements to kill ${monster.name}! Your ${readableStatName( unmetKey! - )} stat in your ${setup} setup is ${has}, but you need atleast ${ + )} stat in your ${setup} setup is ${has}, but you need at least ${ monster.minimumGearRequirements[setup]?.[unmetKey!] }.` ]; diff --git a/src/tasks/minions/HunterActivity/hunterActivity.ts b/src/tasks/minions/HunterActivity/hunterActivity.ts index 914d29dfdd..24994a87f5 100644 --- a/src/tasks/minions/HunterActivity/hunterActivity.ts +++ b/src/tasks/minions/HunterActivity/hunterActivity.ts @@ -178,8 +178,8 @@ export const hunterTask: MinionTask = { let str = `${user}, ${user.minionName} finished hunting ${creature.name}${ crystalImpling - ? '.' - : `${quantity}x times, due to clever creatures you missed out on ${ + ? '. ' + : ` ${quantity}x times, due to clever creatures you missed out on ${ quantity - successfulQuantity }x catches. ` }${xpStr}\n\nYou received: ${loot}.${magicSecStr.length > 1 ? magicSecStr : ''}`; diff --git a/src/tasks/minions/colosseumActivity.ts b/src/tasks/minions/colosseumActivity.ts index 8b38486804..bc0da01e0b 100644 --- a/src/tasks/minions/colosseumActivity.ts +++ b/src/tasks/minions/colosseumActivity.ts @@ -4,8 +4,10 @@ import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { resolveItems } from 'oldschooljs/dist/util/util'; import { ColosseumWaveBank, colosseumWaves } from '../../lib/colosseum'; +import { refundChargeBank } from '../../lib/degradeableItems'; import { trackLoot } from '../../lib/lootTrack'; import { incrementMinigameScore } from '../../lib/settings/minigames'; +import { ChargeBank } from '../../lib/structures/Bank'; import type { ColoTaskOptions } from '../../lib/types/minions'; import { handleTripFinish } from '../../lib/util/handleTripFinish'; import { makeBankImage } from '../../lib/util/makeBankImage'; @@ -17,7 +19,16 @@ const sunfireItems = resolveItems(['Sunfire fanatic helm', 'Sunfire fanatic cuir export const colosseumTask: MinionTask = { type: 'Colosseum', async run(data: ColoTaskOptions) { - const { channelID, userID, loot: possibleLoot, diedAt, maxGlory } = data; + const { + channelID, + userID, + loot: possibleLoot, + diedAt, + maxGlory, + scytheCharges, + venatorBowCharges, + bloodFuryCharges + } = data; const user = await mUserFetch(userID); const newKCs = new ColosseumWaveBank(); @@ -33,20 +44,41 @@ export const colosseumTask: MinionTask = { .map(([kc, amount]) => `Wave ${kc}: ${amount} KC`) .join(', ')}`; + let scytheRefund = 0; + let venatorBowRefund = 0; + let bloodFuryRefund = 0; + const newWaveKcStr = !diedAt || diedAt > 1 ? `New wave KCs: ${newKCsStr}.` : 'No new KCs.'; if (diedAt) { const wave = colosseumWaves.find(i => i.waveNumber === diedAt)!; - return handleTripFinish( - user, - channelID, - `${user}, you died on wave ${diedAt} to ${randArrItem([ - ...(wave?.reinforcements ?? []), - ...wave.enemies - ])}, and received no loot. ${newWaveKcStr}`, - undefined, - data, - null - ); + + let str = `${user}, you died on wave ${diedAt} to ${randArrItem([ + ...(wave?.reinforcements ?? []), + ...wave.enemies + ])}, and received no loot. ${newWaveKcStr}`; + + // Calculate refund for unused charges + const completionPercentage = (diedAt - 1) / 12; + if (scytheCharges > 0) scytheRefund = Math.ceil(scytheCharges * (1 - completionPercentage)); + if (venatorBowCharges > 0) venatorBowRefund = Math.ceil(venatorBowCharges * (1 - completionPercentage)); + if (bloodFuryCharges > 0) bloodFuryRefund = Math.ceil(bloodFuryCharges * (1 - completionPercentage)); + + const chargeBank = new ChargeBank(); + if (scytheRefund > 0) chargeBank.add('scythe_of_vitur_charges', scytheRefund); + if (venatorBowRefund > 0) chargeBank.add('venator_bow_charges', venatorBowRefund); + if (bloodFuryRefund > 0) chargeBank.add('blood_fury_charges', bloodFuryRefund); + + if (chargeBank.length() > 0) { + const refundResults = await refundChargeBank(user, chargeBank); + + const refundMessages = refundResults + .map(result => `${result.userMessage} Total charges: ${result.totalCharges}.`) + .join('\n'); + + str += `\n${refundMessages}`; + } + + return handleTripFinish(user, channelID, str, undefined, data, null); } await incrementMinigameScore(user.id, 'colosseum'); diff --git a/src/tasks/minions/mageArena2Activity.ts b/src/tasks/minions/mageArena2Activity.ts index 4a348b5e13..7cc092ecf6 100644 --- a/src/tasks/minions/mageArena2Activity.ts +++ b/src/tasks/minions/mageArena2Activity.ts @@ -14,12 +14,12 @@ export const mageArenaTwoTask: MinionTask = { let loot: Bank | undefined = undefined; if (percentChance(70)) { const deathReason = randArrItem([ - 'Died to Porazdir.', - 'Killed by Derwen.', - 'Killed by Justiciar Zachariah.', - "PK'd by a clan.", - 'Killed by Chaos Elemental.', - 'Killed by a PKer.' + 'Died to Porazdir', + 'Killed by Derwen', + 'Killed by Justiciar Zachariah', + "PK'd by a clan", + 'Killed by Chaos Elemental', + 'Killed by a PKer' ]); str = `${user}, ${user.minionName} failed to complete the Mage Arena II: ${deathReason}. Try again.`; } else { diff --git a/src/tasks/minions/minigames/fightCavesActivity.ts b/src/tasks/minions/minigames/fightCavesActivity.ts index f4ced983d9..a5cb2e71c2 100644 --- a/src/tasks/minions/minigames/fightCavesActivity.ts +++ b/src/tasks/minions/minigames/fightCavesActivity.ts @@ -35,7 +35,7 @@ export const fightCavesTask: MinionTask = { { fight_caves_attempts: true } ); - const attemptsStr = `You have tried Fight caves ${newFightCavesAttempts}x times.`; + const attemptsStr = `You have tried Fight caves ${newFightCavesAttempts}x times`; // Add slayer const usersTask = await getUsersCurrentSlayerInfo(user.id); @@ -81,7 +81,7 @@ export const fightCavesTask: MinionTask = { preJadDeathTime )} into your attempt.${slayerMsg} The following supplies were refunded back into your bank: ${itemLootBank}.`, await chatHeadImage({ - content: `You die before you even reach TzTok-Jad...atleast you tried, I give you ${tokkulReward}x Tokkul. ${attemptsStr}`, + content: `You die before you even reach TzTok-Jad... At least you tried, I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, head: 'mejJal' }), data, @@ -117,7 +117,7 @@ export const fightCavesTask: MinionTask = { channelID, `${user} ${msg}`, await chatHeadImage({ - content: `TzTok-Jad stomp you to death...nice try though JalYt, for your effort I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, + content: `TzTok-Jad stomp you to death... Nice try though JalYt, for your effort I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, head: 'mejJal' }), data, @@ -152,8 +152,8 @@ export const fightCavesTask: MinionTask = { itemsToAdd: loot }); - const rangeXP = await user.addXP({ skillName: SkillsEnum.Ranged, amount: 47_580, duration }); - const hpXP = await user.addXP({ skillName: SkillsEnum.Hitpoints, amount: 15_860, duration }); + const rangeXP = await user.addXP({ skillName: SkillsEnum.Ranged, amount: 47_580, duration, minimal: true }); + const hpXP = await user.addXP({ skillName: SkillsEnum.Hitpoints, amount: 15_860, duration, minimal: true }); let msg = `${rangeXP}. ${hpXP}.`; if (isOnTask) { @@ -186,7 +186,13 @@ export const fightCavesTask: MinionTask = { } }); - const slayXP = await user.addXP({ skillName: SkillsEnum.Slayer, amount: slayerXP, duration }); + const slayXP = await user.addXP({ + skillName: SkillsEnum.Slayer, + amount: slayerXP, + duration, + minimal: true + }); + const xpMessage = `${msg} ${slayXP}`; msg = `Jad task completed. ${xpMessage}. \n**You've completed ${currentStreak} tasks and received ${points} points; giving you a total of ${secondNewUser.newUser.slayer_points}; return to a Slayer master.**`; diff --git a/src/tasks/minions/minigames/infernoActivity.ts b/src/tasks/minions/minigames/infernoActivity.ts index c5dc66f388..02693b14b7 100644 --- a/src/tasks/minions/minigames/infernoActivity.ts +++ b/src/tasks/minions/minigames/infernoActivity.ts @@ -51,33 +51,43 @@ export const infernoTask: MinionTask = { const [hasDiary] = await userhasDiaryTier(user, diariesObject.KaramjaDiary.elite); if (hasDiary) tokkul *= 2; const baseBank = new Bank().add('Tokkul', tokkul); - - let xpStr = await user.addXP({ - skillName: SkillsEnum.Ranged, - amount: calcPercentOfNum(percentMadeItThrough, 80_000), - duration, - minimal: true - }); - xpStr += await user.addXP({ - skillName: SkillsEnum.Hitpoints, - amount: calcPercentOfNum(percentMadeItThrough, 35_000), - duration, - minimal: true - }); - xpStr += await user.addXP({ - skillName: SkillsEnum.Magic, - amount: calcPercentOfNum(percentMadeItThrough, 25_000), - duration, - minimal: true - }); + const xpBonuses = []; + + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Ranged, + amount: calcPercentOfNum(percentMadeItThrough, 80_000), + duration, + minimal: true + }) + ); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Hitpoints, + amount: calcPercentOfNum(percentMadeItThrough, 35_000), + duration, + minimal: true + }) + ); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Magic, + amount: calcPercentOfNum(percentMadeItThrough, 25_000), + duration, + minimal: true + }) + ); if (isOnTask) { - xpStr += await user.addXP({ - skillName: SkillsEnum.Slayer, - amount: deathTime === null ? 125_000 : calcPercentOfNum(percentMadeItThrough, 25_000), - duration - }); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Slayer, + amount: deathTime === null ? 125_000 : calcPercentOfNum(percentMadeItThrough, 25_000), + duration + }) + ); } + const xpStr = xpBonuses.join(', '); if (!deathTime) { await incrementMinigameScore(userID, 'inferno', 1); } @@ -156,7 +166,7 @@ export const infernoTask: MinionTask = { if (diedPreZuk) { text += `You died ${formatDuration(deathTime!)} into your attempt, before you reached Zuk.`; - chatText = `You die before you even reach TzKal-Zuk...atleast you tried, I give you ${baseBank.amount( + chatText = `You die before you even reach TzKal-Zuk... At least you tried, I give you ${baseBank.amount( 'Tokkul' )}x Tokkul.`; } else if (diedZuk) { diff --git a/tests/integration/clArrayUpdate.test.ts b/tests/integration/clArrayUpdate.test.ts index 9c190aca5a..15f0917f16 100644 --- a/tests/integration/clArrayUpdate.test.ts +++ b/tests/integration/clArrayUpdate.test.ts @@ -1,4 +1,3 @@ -import { Time } from 'e'; import { expect, test } from 'vitest'; import { Bank } from 'oldschooljs'; @@ -6,39 +5,33 @@ import { itemID } from 'oldschooljs/dist/util'; import { roboChimpSyncData } from '../../src/lib/roboChimp'; import { createTestUser } from './util'; -test( - 'All Commands Base Test', - async () => { - const user = await createTestUser(); - await user.addItemsToBank({ items: new Bank().add('Coal', 100) }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [], - cl_array_length: 0 - }); +test('CL Updates', async () => { + const user = await createTestUser(); + await user.addItemsToBank({ items: new Bank().add('Coal', 100) }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [], + cl_array_length: 0 + }); - await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Egg')], - cl_array_length: 1 - }); + await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Egg')], + cl_array_length: 1 + }); - await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Egg')], - cl_array_length: 1 - }); + await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Egg')], + cl_array_length: 1 + }); - await user.addItemsToBank({ items: new Bank().add('Trout', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Trout'), itemID('Egg')], - cl_array_length: 2 - }); - }, - { - timeout: Time.Minute * 10 - } -); + await user.addItemsToBank({ items: new Bank().add('Trout', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Trout'), itemID('Egg')], + cl_array_length: 2 + }); +}); diff --git a/tests/integration/misc.test.ts b/tests/integration/misc.test.ts index 2d113c1b15..eae4db6a9a 100644 --- a/tests/integration/misc.test.ts +++ b/tests/integration/misc.test.ts @@ -1,4 +1,3 @@ -import type { UserEvent } from '@prisma/client'; import { randArrItem } from 'e'; import { describe, expect, test } from 'vitest'; @@ -18,30 +17,14 @@ describe('Integration Misc', () => { expect(await global.prisma!.analytic.count()).toBeGreaterThanOrEqual(1); }); test('fetchCLLeaderboard', async () => { + const cl = randArrItem(allCollectionLogsFlat); for (const ironManOnly of [true, false]) { - for (const method of ['cl_array', 'raw_cl'] as const) { - for (const userEvents of [ - [ - { - id: 'asdf', - date: new Date(), - user_id: '123', - type: 'CLCompletion', - skill: null, - collection_log_name: 'giant mole' - } as UserEvent - ], - null - ]) { - await fetchCLLeaderboard({ - ironmenOnly: ironManOnly, - method, - userEvents, - resultLimit: 100, - items: randArrItem(allCollectionLogsFlat).items - }); - } - } + await fetchCLLeaderboard({ + ironmenOnly: ironManOnly, + resultLimit: 100, + items: cl.items, + clName: cl.name + }); } await Promise.all([fetchCLLeaderboard]); }); diff --git a/tests/integration/redis.test.ts b/tests/integration/redis.test.ts index ead9262f7e..1407d57583 100644 --- a/tests/integration/redis.test.ts +++ b/tests/integration/redis.test.ts @@ -11,11 +11,11 @@ function makeSender() { return new TSRedis({ mocked: !globalConfig.redisPort, port: globalConfig.redisPort }); } -test.concurrent('Should add patron badge', async () => { +test('Should add patron badge', async () => { const user = await createTestUser(); expect(user.user.badges).not.includes(BadgesEnum.Patron); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: [user.id], new_tier: 1, @@ -27,11 +27,11 @@ test.concurrent('Should add patron badge', async () => { expect(user.user.badges).includes(BadgesEnum.Patron); }); -test.concurrent('Should remove patron badge', async () => { +test('Should remove patron badge', async () => { const user = await createTestUser(undefined, { badges: [BadgesEnum.Patron] }); expect(user.user.badges).includes(BadgesEnum.Patron); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: [user.id], new_tier: 0, @@ -43,7 +43,7 @@ test.concurrent('Should remove patron badge', async () => { expect(user.user.badges).not.includes(BadgesEnum.Patron); }); -test.concurrent('Should add to cache', async () => { +test('Should add to cache', async () => { const users = [await createTestUser(), await createTestUser(), await createTestUser()]; await roboChimpClient.user.createMany({ data: users.map(u => ({ @@ -52,7 +52,7 @@ test.concurrent('Should add to cache', async () => { })) }); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: users.map(u => u.id), new_tier: 5, @@ -67,7 +67,7 @@ test.concurrent('Should add to cache', async () => { } }); -test.concurrent('Should remove from cache', async () => { +test('Should remove from cache', async () => { const users = [await createTestUser(), await createTestUser(), await createTestUser()]; await roboChimpClient.user.createMany({ data: users.map(u => ({ @@ -76,7 +76,7 @@ test.concurrent('Should remove from cache', async () => { })) }); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: users.map(u => u.id), new_tier: 0, @@ -91,7 +91,7 @@ test.concurrent('Should remove from cache', async () => { } }); -test.concurrent('Should recognize special bitfields', async () => { +test('Should recognize special bitfields', async () => { const users = [ await createTestUser(undefined, { bitfield: [BitField.HasPermanentTierOne] }), await createTestUser(undefined, { bitfield: [BitField.BothBotsMaxedFreeTierOnePerks] }) @@ -101,7 +101,7 @@ test.concurrent('Should recognize special bitfields', async () => { } }); -test.concurrent('Should sdffsddfss', async () => { +test('Should sdffsddfss', async () => { const user = await createTestUser(); roboChimpCache.set(user.id, { perk_tier: 5 } as any); expect(getUsersPerkTier(user)).toEqual(5); diff --git a/tests/integration/rolesTask.test.ts b/tests/integration/rolesTask.test.ts index 252bee8faf..dcd69d0dcc 100644 --- a/tests/integration/rolesTask.test.ts +++ b/tests/integration/rolesTask.test.ts @@ -43,7 +43,7 @@ describe.skip('Roles Task', async () => { duration: 10_000 } }); - const result = await runRolesTask(); + const result = await runRolesTask(true); expect(result).toBeTruthy(); expect(result).includes('Roles'); }); diff --git a/tests/unit/snapshots/clsnapshots.test.ts.snap b/tests/unit/snapshots/clsnapshots.test.ts.snap index 1a39a3fc00..8a486bc15e 100644 --- a/tests/unit/snapshots/clsnapshots.test.ts.snap +++ b/tests/unit/snapshots/clsnapshots.test.ts.snap @@ -37,7 +37,7 @@ Elite Treasure Trails (59) Farming (141) Fishing Trawler (4) Forestry (23) -Fortis Colosseum (8) +Fortis Colosseum (9) Fossil Island Notes (10) General Graardor (8) Giant Mole (3) diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts index 46b6b535fe..91dc5c1bc9 100644 --- a/vitest.integration.config.mts +++ b/vitest.integration.config.mts @@ -7,8 +7,7 @@ export default defineConfig({ setupFiles: 'tests/integration/setup.ts', coverage: { provider: 'v8', - reporter: 'text', - include: ['src/lib/MUser.ts'] + reporter: 'text' }, testTimeout: 30_000, bail: 1, diff --git a/yarn.lock b/yarn.lock index 7e6345566a..5dfb8015f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -646,9 +646,9 @@ __metadata: languageName: node linkType: hard -"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#3550582efbdf04929f0a2c5114ed069a61551807": +"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed": version: 0.0.24 - resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=3550582efbdf04929f0a2c5114ed069a61551807" + resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed" dependencies: decimal.js: "npm:^10.4.3" deep-object-diff: "npm:^1.1.9" @@ -664,7 +664,7 @@ __metadata: peerDependencies: discord.js: ^14.15.3 oldschooljs: ^2.5.9 - checksum: 10c0/8a968e143bc34c38d15ab29f1ecde613340e58c93341d51368651c6af52228652449be13a5a1c299bc836e0ff9a5a9c7c6a104198096d73345dd3601e06655ae + checksum: 10c0/42eaec1c99c671adab7b56ca7e11d37bf5a0e07d0f0da0a892cdf477a78c061ea131a43b1c578d09f1c6b02e05d1ce47db9586ad9a8de62679cc492c847c3fca languageName: node linkType: hard @@ -1217,6 +1217,13 @@ __metadata: languageName: node linkType: hard +"@sapphire/timer-manager@npm:^1.0.2": + version: 1.0.2 + resolution: "@sapphire/timer-manager@npm:1.0.2" + checksum: 10c0/0c9fee9a94b5927020aa98dac0fbe7ab903fc8b028383ae28c625f262eec4b65973b5ba5a17d329f72e1abf2092e7655647517d8818378781d5b790cb8b2e62a + languageName: node + linkType: hard + "@sapphire/utilities@npm:^3.3.0": version: 3.3.0 resolution: "@sapphire/utilities@npm:3.3.0" @@ -2412,6 +2419,13 @@ __metadata: languageName: node linkType: hard +"exit-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "exit-hook@npm:4.0.0" + checksum: 10c0/7fb33eaeb9050aee9479da9c93d42b796fb409c40e1d2b6ea2f40786ae7d7db6dc6a0f6ecc7bc24e479f957b7844bcb880044ded73320334743c64e3ecef48d7 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -3958,6 +3972,15 @@ __metadata: languageName: node linkType: hard +"remeda@npm:^2.7.0": + version: 2.7.0 + resolution: "remeda@npm:2.7.0" + dependencies: + type-fest: "npm:^4.21.0" + checksum: 10c0/4e7d0dc616f00961653244ea9df3f297720fc9346ac8ec7502abf4c434741af4a4750d5bd83ea9938ee406089b37e3a2270b8f022d48b345ba83218e47dd8918 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -4099,11 +4122,12 @@ __metadata: dependencies: "@biomejs/biome": "npm:^1.8.3" "@napi-rs/canvas": "npm:^0.1.53" - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#3550582efbdf04929f0a2c5114ed069a61551807" + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed" "@prisma/client": "npm:^5.17.0" "@sapphire/ratelimits": "npm:^2.4.9" "@sapphire/snowflake": "npm:^3.5.3" "@sapphire/time-utilities": "npm:^1.6.0" + "@sapphire/timer-manager": "npm:^1.0.2" "@sentry/node": "npm:^8.15.0" "@types/lodash": "npm:^4.14.195" "@types/node": "npm:^20.14.9" @@ -4118,6 +4142,7 @@ __metadata: dpdm: "npm:^3.14.0" e: "npm:0.2.33" esbuild: "npm:0.21.5" + exit-hook: "npm:^4.0.0" fast-deep-equal: "npm:^3.1.3" fast-glob: "npm:^3.3.2" lodash: "npm:^4.17.21" @@ -4132,6 +4157,7 @@ __metadata: prettier: "npm:^3.3.2" prisma: "npm:^5.17.0" random-js: "npm:^2.1.0" + remeda: "npm:^2.7.0" simple-statistics: "npm:^7.8.3" sonic-boom: "npm:^4.0.1" tsx: "npm:^4.16.2" @@ -4597,6 +4623,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.21.0": + version: 4.23.0 + resolution: "type-fest@npm:4.23.0" + checksum: 10c0/c42bb14e99329ab37983d1f188e307bf0cc705a23807d9b2268d8fb2ae781d610ac6e2058dde8f9ea2b1b8ddc77ceb578d157fa81f69f8f70aef1d42fb002996 + languageName: node + linkType: hard + "typescript@npm:^5.2.2, typescript@npm:^5.5.3": version: 5.5.3 resolution: "typescript@npm:5.5.3"