diff --git a/data/bso.commands.json b/data/bso.commands.json index 6427f95b2a..28b13aa8a4 100644 --- a/data/bso.commands.json +++ b/data/bso.commands.json @@ -101,6 +101,11 @@ "examples": ["/chop name:Logs"], "subOptions": [] }, + { + "name": "christmas", + "desc": "The 2024 Christmas Event", + "subOptions": ["check", "active_cast", "drop_rates"] + }, { "name": "cl", "desc": "See your Collection Log.", @@ -251,11 +256,6 @@ "desc": "See your current GP balance.", "subOptions": [] }, - { - "name": "halloween", - "desc": "The 2024 Halloween Event", - "subOptions": ["check", "hand_in"] - }, { "name": "help", "desc": "Get information and help with the bot.", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5f354622e2..c70a314e40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1247,6 +1247,7 @@ enum activity_type_enum { MyNotes Colosseum CreateForestersRations + SnoozeSpellActive } enum xp_gains_skill_enum { diff --git a/src/lib/Task.ts b/src/lib/Task.ts index 434d0eea8a..554a3536ef 100644 --- a/src/lib/Task.ts +++ b/src/lib/Task.ts @@ -32,6 +32,7 @@ import { naxxusTask } from '../tasks/minions/bso/naxxusActivity'; import { nexTask } from '../tasks/minions/bso/nexActivity'; import { odsTask } from '../tasks/minions/bso/ouraniaDeliveryServiceActivity'; import { researchActivityTask } from '../tasks/minions/bso/researchActivity'; +import { snoozeSpellActiveTask } from '../tasks/minions/bso/snoozeActivity.js'; import { scTask } from '../tasks/minions/bso/stealingCreationActivity'; import { twTask } from '../tasks/minions/bso/tinkeringWorkshopActivity'; import { turaelsTrialsTask } from '../tasks/minions/bso/turaelsTrialsActivity'; @@ -239,7 +240,8 @@ const tasks: MinionTask[] = [ turaelsTrialsTask, myNotesTask, colosseumTask, - CreateForestersRationsTask + CreateForestersRationsTask, + snoozeSpellActiveTask ]; export async function processPendingActivities() { diff --git a/src/lib/christmasEvent.ts b/src/lib/christmasEvent.ts new file mode 100644 index 0000000000..4e68612935 --- /dev/null +++ b/src/lib/christmasEvent.ts @@ -0,0 +1,76 @@ +import { resolveItems } from './util.js'; + +const wizardOutfit = resolveItems([ + 'Snowdream robe legs', + 'Snowdream wizard hat', + 'Snowdream robe top', + 'Snowdream wizard socks' +]); + +export const blackSantaOutfit = resolveItems([ + 'Black santa top', + 'Black santa legs', + 'Black santa gloves', + 'Black santa boots' +]); + +export const allChristmasEventItems = resolveItems([ + ...wizardOutfit, + ...blackSantaOutfit, + 'Snowdream rune', + 'Snowdream pillow', + 'Chef-touched heart (choc)', + 'Sungod slippers', + 'Icey santa hat', + 'Grinchling', + 'Shrimpy', + 'Snowflake amulet', + 'Snowdream staff' +]); + +const daysOnAverageChance = (days: number) => days * 1440; + +interface Drop { + items: number[]; + chancePerMinute: number; + clDropRateIncrease?: number; +} + +export const christmasDroprates: Drop[] = [ + { + items: wizardOutfit, + chancePerMinute: daysOnAverageChance(0.3), + clDropRateIncrease: 4 + }, + { + items: resolveItems(['Icey santa hat']), + chancePerMinute: daysOnAverageChance(10) + }, + { + items: blackSantaOutfit, + chancePerMinute: daysOnAverageChance(0.8), + clDropRateIncrease: 3 + }, + { + items: resolveItems(['Snowdream pillow']), + chancePerMinute: daysOnAverageChance(1.5), + clDropRateIncrease: 2 + }, + { + items: resolveItems(['Chef-touched heart (choc)']), + chancePerMinute: daysOnAverageChance(0.2), + clDropRateIncrease: 2 + }, + { + items: resolveItems(['Sungod slippers']), + chancePerMinute: daysOnAverageChance(1.5), + clDropRateIncrease: 3 + }, + { + items: resolveItems(['Shrimpy', 'Grinchling']), + chancePerMinute: daysOnAverageChance(5), + clDropRateIncrease: 2 + } +]; + +export const SNOWDREAM_RUNES_PER_MINUTE = 30; diff --git a/src/lib/customItems/customItems.ts b/src/lib/customItems/customItems.ts index b5a89e50f9..15b9727d93 100644 --- a/src/lib/customItems/customItems.ts +++ b/src/lib/customItems/customItems.ts @@ -13521,3 +13521,221 @@ setCustomItem( }, 500_000 ); + +setCustomItem( + 73_304, + 'Snowdream logs', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_305, + 'Snowdream staff', + 'Bronze dagger', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_306, + 'Snowdream rune', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_307, + 'Snowdream robe legs', + 'Bronze platelegs', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_308, + 'Snowdream wizard hat', + 'Bronze full helm', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_309, + 'Snowdream robe top', + 'Bronze full helm', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_310, + 'Snowdream wizard socks', + 'Bronze boots', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_311, + 'Snowdream pillow', + 'Bronze kiteshield', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_312, + 'Black santa top', + 'Bronze platebody', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_313, + 'Black santa legs', + 'Bronze platelegs', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_314, + 'Black santa gloves', + 'Bronze gloves', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_315, + 'Black santa boots', + 'Bronze boots', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_316, + 'Chef-touched heart (choc)', + 'Coal', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_317, + 'Icey santa hat', + 'Bronze full helm', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_318, + 'Sungod slippers', + 'Bronze boots', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 500 +); + +setCustomItem( + 73_319, + 'Axe of the high sungod (xmas)', + 'Axe of the high sungod', + { + customItemData: { + cantDropFromMysteryBoxes: true, + isSuperUntradeable: true + } + }, + 100_000 +); + +setCustomItem( + 73_320, + 'Grinchling', + 'Herbi', + { + customItemData: { + cantDropFromMysteryBoxes: true + } + }, + 100_000 +); + +setCustomItem( + 73_321, + 'Snowflake amulet', + 'Amulet of strength', + { + customItemData: { + cantDropFromMysteryBoxes: true, + isSuperUntradeable: true + } + }, + 100_000 +); diff --git a/src/lib/data/Collections.ts b/src/lib/data/Collections.ts index a5894328f7..a8bb5623d2 100644 --- a/src/lib/data/Collections.ts +++ b/src/lib/data/Collections.ts @@ -3,6 +3,7 @@ import { calcWhatPercent, isObject, notEmpty, removeFromArr, sumArr, uniqueArr } import { Bank, ChambersOfXeric, Clues, type Item, type Monster, Monsters } from 'oldschooljs'; import { resolveItems } from 'oldschooljs/dist/util/util'; import { divinationEnergies, portents } from '../bso/divination'; +import { allChristmasEventItems } from '../christmasEvent.js'; import type { ClueTier } from '../clues/clueTiers'; import { ClueTiers } from '../clues/clueTiers'; import type { CollectionLogType } from '../collectionLogTask'; @@ -49,6 +50,7 @@ import { MUserStats } from '../structures/MUserStats'; import { getAllIgneTameKCs, tameKillableMonsters } from '../tames'; import type { ItemBank } from '../types'; import getOSItem from '../util/getOSItem'; +import itemID from '../util/itemID.js'; import { shuffleRandom } from '../util/smallUtils'; import type { FormatProgressFunction, ICollection, ILeftListStatus, IToReturnCollection } from './CollectionsExport'; import { @@ -1863,6 +1865,11 @@ export const allCollectionLogs: ICollection = { ]), counts: false }, + 'Christmas 2024': { + alias: ['xmas 2024', 'christmas 2024'], + items: allChristmasEventItems.filter(i => i !== itemID('Snowflake amulet')), + counts: false + }, 'BSO Birthday 2022': { alias: ['bso birthday 2022'], items: resolveItems(['Honey', 'Honeycomb', 'Beehive', 'Buzz']), diff --git a/src/lib/data/CollectionsExport.ts b/src/lib/data/CollectionsExport.ts index 4650a513d8..af56c4f470 100644 --- a/src/lib/data/CollectionsExport.ts +++ b/src/lib/data/CollectionsExport.ts @@ -2534,7 +2534,9 @@ export const discontinuedCustomPetsCL = resolveItems([ 'Rudolph', 'Cluckers', 'Polterpup', - 'Mumpkin' + 'Mumpkin', + 'Grinchling', + 'Shrimpy' ]); export const kingGoldemarCL = resolveItems([ diff --git a/src/lib/data/creatables/bsoItems.ts b/src/lib/data/creatables/bsoItems.ts index a0f19706c0..47f1d4e337 100644 --- a/src/lib/data/creatables/bsoItems.ts +++ b/src/lib/data/creatables/bsoItems.ts @@ -1395,6 +1395,11 @@ export const BsoCreateables: Createable[] = [ .add('Pernix components', 3), outputItems: new Bank().add('Deathly collector (i)'), noCreatablesCl: true + }, + { + name: 'Snowdream staff', + inputItems: new Bank().add('Snowdream logs', 8), + outputItems: new Bank().add('Snowdream staff') } ]; diff --git a/src/lib/data/creatablesTable.txt b/src/lib/data/creatablesTable.txt index 3a59792695..3e3393be50 100644 --- a/src/lib/data/creatablesTable.txt +++ b/src/lib/data/creatablesTable.txt @@ -991,6 +991,7 @@ | Clue scroll (elder) | 3x Elder scroll piece | 1x Clue scroll (elder) | 0 | | Tidal collector (i) | 10x Armadylean components, 5x Blessed dizana's quiver, 4x Masori components, 3x Pernix components, 1x Tidal collector | 1x Tidal collector (i) | 0 | | Deathly collector (i) | 10x Armadylean components, 5x Blessed dizana's quiver, 1x Deathly collector, 4x Masori components, 3x Pernix components | 1x Deathly collector (i) | 0 | +| Snowdream staff | 8x Snowdream logs | 1x Snowdream staff | 0 | | Lumina (Elder logs) | 5x Elder logs, 30x Elder rune | 1x Lumina | 0 | | Lumina (Redwood logs) | 30x Elder rune, 10x Redwood logs | 1x Lumina | 0 | | Lumina (Magic logs) | 30x Elder rune, 30x Magic logs | 1x Lumina | 0 | diff --git a/src/lib/resources/images/magnaboy.png b/src/lib/resources/images/magnaboy.png new file mode 100644 index 0000000000..e5395ef68d Binary files /dev/null and b/src/lib/resources/images/magnaboy.png differ diff --git a/src/lib/skilling/skills/runecraft.ts b/src/lib/skilling/skills/runecraft.ts index 12d124565c..cd438b4676 100644 --- a/src/lib/skilling/skills/runecraft.ts +++ b/src/lib/skilling/skills/runecraft.ts @@ -28,6 +28,13 @@ interface Tiara { } const Runes: Rune[] = [ + { + xp: 5, + id: itemID('Snowdream rune'), + name: 'Snowdream rune', + levels: [[1, 1]], + tripLength: Time.Minute * 1.917 + }, { xp: 5, id: itemID('Air rune'), diff --git a/src/lib/skilling/skills/woodcutting/woodcutting.ts b/src/lib/skilling/skills/woodcutting/woodcutting.ts index bf2dffbafc..8d77d7124d 100644 --- a/src/lib/skilling/skills/woodcutting/woodcutting.ts +++ b/src/lib/skilling/skills/woodcutting/woodcutting.ts @@ -16,6 +16,21 @@ const sulliuscepTable = new LootTable() .oneIn(700, 'Unidentified rare fossil'); const logs: Log[] = [ + { + level: 1, + xp: 25, + id: itemID('Snowdream logs'), + name: 'Snowdream logs', + findNewTreeTime: 100, + bankingTime: 50, + slope: 0.54, + intercept: 24.85, + depletionChance: 100, + wcGuild: true, + petChance: 317_647, + qpRequired: 0, + clueScrollChance: 317_647 + }, { level: 1, xp: 25, diff --git a/src/lib/types/minions.ts b/src/lib/types/minions.ts index 02b6d7aef6..136c0c3469 100644 --- a/src/lib/types/minions.ts +++ b/src/lib/types/minions.ts @@ -735,6 +735,11 @@ export interface MemoryHarvestOptions extends ActivityTaskOptions { r: number; } +export interface SnoozeSpellActiveCastOptions extends ActivityTaskOptions { + type: 'SnoozeSpellActive'; + hours: number; +} + export type ActivityTaskData = | MonsterActivityTaskOptions | WoodcuttingActivityTaskOptions @@ -818,4 +823,5 @@ export type ActivityTaskData = | TuraelsTrialsOptions | CutLeapingFishActivityTaskOptions | CreateForestersRationsActivityTaskOptions - | ColoTaskOptions; + | ColoTaskOptions + | SnoozeSpellActiveCastOptions; diff --git a/src/lib/util/chatHeadImage.ts b/src/lib/util/chatHeadImage.ts index 583fcc8e4f..64f4a242c2 100644 --- a/src/lib/util/chatHeadImage.ts +++ b/src/lib/util/chatHeadImage.ts @@ -13,6 +13,7 @@ const gertrudeChatHead = loadAndCacheLocalImage('./src/lib/resources/images/gert const antiSantaChatHead = loadAndCacheLocalImage('./src/lib/resources/images/antisanta.png'); const bunnyChatHead = loadAndCacheLocalImage('./src/lib/resources/images/bunny.png'); const monkeyChildChatHead = loadAndCacheLocalImage('./src/lib/resources/images/monkeychild.png'); +const magnaboyChatHead = loadAndCacheLocalImage('./src/lib/resources/images/magnaboy.png'); const marimboChatHead = loadAndCacheLocalImage('./src/lib/resources/images/marimbo.png'); const partyPeteHead = loadAndCacheLocalImage('./src/lib/resources/images/partyPete.png'); const mysteriousFigureHead = loadAndCacheLocalImage('./src/lib/resources/images/mysteriousFigure.png'); @@ -29,6 +30,7 @@ const chatHeads = { izzy: izzyChatHead, alry: alryTheAnglerChatHead, wurMuTheMonkey: monkeyChildChatHead, + magnaboy: magnaboyChatHead, marimbo: marimboChatHead, ketKeh: ketKehChatHead, gertrude: gertrudeChatHead, @@ -59,7 +61,8 @@ const names: Record = { rudolph: 'Rudolph the Reindeer', minimus: 'Minimus', pumpkin: 'Pumpkinhead', - spookling: 'Spookling' + spookling: 'Spookling', + magnaboy: 'Magnaboy' }; export async function newChatHeadImage({ content, head }: { content: string; head: keyof typeof chatHeads }) { diff --git a/src/lib/util/handleCrateSpawns.ts b/src/lib/util/handleCrateSpawns.ts index 96bd444cc6..61eba291aa 100644 --- a/src/lib/util/handleCrateSpawns.ts +++ b/src/lib/util/handleCrateSpawns.ts @@ -6,7 +6,7 @@ import { itemNameFromID } from './smallUtils'; const crateItem = getOSItem('Frozen crate (s8)'); -const xmasPets = resolveItems(['Smokey', 'Rudolph', 'Frosty']); +const xmasPets = resolveItems(['Smokey', 'Rudolph', 'Frosty', 'Grinchling', 'Shrimpy']); export function handleCrateSpawns(user: MUser, duration: number, messages?: string[]) { const accountAge = user.accountAgeInDays(); diff --git a/src/lib/util/handleTripFinish.ts b/src/lib/util/handleTripFinish.ts index 3afda0bdb5..fae17d3573 100644 --- a/src/lib/util/handleTripFinish.ts +++ b/src/lib/util/handleTripFinish.ts @@ -1,4 +1,5 @@ import { channelIsSendable, makeComponents, mentionCommand } from '@oldschoolgg/toolkit'; +import { Stopwatch } from '@oldschoolgg/toolkit/structures'; import { activity_type_enum } from '@prisma/client'; import { type AttachmentBuilder, @@ -7,10 +8,9 @@ import { type MessageCreateOptions, bold } from 'discord.js'; +import { Time, notEmpty, randArrItem, randInt } from 'e'; import { Bank } from 'oldschooljs'; -import { Stopwatch } from '@oldschoolgg/toolkit/structures'; -import { Time, notEmpty, randArrItem, randInt } from 'e'; import { alching } from '../../mahoji/commands/laps'; import { calculateBirdhouseDetails } from '../../mahoji/lib/abstracted_commands/birdhousesCommand'; import { canRunAutoContract } from '../../mahoji/lib/abstracted_commands/farmingContractCommand'; @@ -19,6 +19,7 @@ import { updateClientGPTrackSetting, userStatsBankUpdate, userStatsUpdate } from import { PortentID, chargePortentIfHasCharges, getAllPortentCharges } from '../bso/divination'; import { gods } from '../bso/divineDominion'; import { MysteryBoxes } from '../bsoOpenables'; +import { SNOWDREAM_RUNES_PER_MINUTE, christmasDroprates } from '../christmasEvent.js'; import { ClueTiers } from '../clues/clueTiers'; import { buildClueButtons } from '../clues/clueUtils'; import { combatAchievementTripEffect } from '../combat_achievements/combatAchievements'; @@ -47,8 +48,8 @@ import { } from './globalInteractions'; import { handleCrateSpawns } from './handleCrateSpawns'; import itemID from './itemID'; -import { logError } from './logError'; -import { perHourChance } from './smallUtils'; +import { assert, logError } from './logError'; +import { itemNameFromID, perHourChance } from './smallUtils'; import { updateBankSetting } from './updateBankSetting'; import { sendToChannelID } from './webhook'; @@ -475,6 +476,41 @@ const tripFinishEffects: TripFinishEffect[] = [ }; } } + }, + { + name: 'Snooze spell passive', + fn: async ({ data, user, messages }) => { + if ( + !user.hasEquippedOrInBank('Snowdream staff') || + !user.owns('Snowdream rune') || + data.type === 'SnoozeSpellActive' + ) { + return; + } + + const minutes = Math.floor(data.duration / Time.Minute); + if (minutes < 1) return; + const spellsCast = Math.min(minutes, user.bank.amount('Snowdream rune') / SNOWDREAM_RUNES_PER_MINUTE); + if (spellsCast < 1) return; + const runeCost = new Bank().add('Snowdream rune', spellsCast * SNOWDREAM_RUNES_PER_MINUTE); + assert(user.bank.has(runeCost), 'User does not have enough runes to cast Snooze spell'); + const loot = new Bank(); + for (const drop of christmasDroprates) { + let dropRate = drop.chancePerMinute; + if (drop.clDropRateIncrease && user.cl.amount(drop.items[0]) >= 1) { + dropRate *= user.cl.amount(drop.items[0]) * drop.clDropRateIncrease; + } + messages.push( + `\`${spellsCast} rolls at 1/${dropRate} chance of ${drop.items.map(itemNameFromID).join('+')}\`` + ); + for (let i = 0; i < spellsCast; i++) { + if (roll(dropRate)) { + for (const item of drop.items) loot.add(item); + } + } + } + await user.transactItems({ itemsToAdd: loot, collectionLog: true, itemsToRemove: runeCost }); + } } ]; diff --git a/src/lib/util/repeatStoredTrip.ts b/src/lib/util/repeatStoredTrip.ts index 4f42ee1f50..a1caa3c857 100644 --- a/src/lib/util/repeatStoredTrip.ts +++ b/src/lib/util/repeatStoredTrip.ts @@ -66,6 +66,7 @@ import type { ShadesOfMortonOptions, SmeltingActivityTaskOptions, SmithingActivityTaskOptions, + SnoozeSpellActiveCastOptions, TOAOptions, TempleTrekkingActivityTaskOptions, TheatreOfBloodTaskOptions, @@ -869,6 +870,14 @@ export const tripHandlers = { name: 'colosseum', quantity: data.quantity }) + }, + [activity_type_enum.SnoozeSpellActive]: { + commandName: 'christmas', + args: (data: SnoozeSpellActiveCastOptions) => ({ + active_cast: { + hours: data.hours + } + }) } } as const; @@ -930,6 +939,7 @@ export async function repeatTrip( return interactionReply(interaction, { content: "Couldn't find any trip to repeat.", ephemeral: true }); } const handler = tripHandlers[data.type]; + return runCommand({ commandName: handler.commandName, isContinue: true, diff --git a/src/mahoji/commands/allCommands.ts b/src/mahoji/commands/allCommands.ts index 287f04059d..2a804ee5f5 100644 --- a/src/mahoji/commands/allCommands.ts +++ b/src/mahoji/commands/allCommands.ts @@ -14,6 +14,7 @@ import { caCommand } from './ca'; import { casketCommand } from './casket'; import { chooseCommand } from './choose'; import { chopCommand } from './chop'; +import { christmasCommand } from './christmas'; import { collectionLogCommand } from './cl'; import { claimCommand } from './claim'; import { clueCommand } from './clue'; @@ -40,7 +41,6 @@ import { gearPresetsCommand } from './gearpresets'; import { giftCommand } from './gift'; import { giveawayCommand } from './giveaway'; import { gpCommand } from './gp'; -import { halloweenCommand } from './halloween'; import { helpCommand } from './help'; import { huntCommand } from './hunt'; import { icCommand } from './ic'; @@ -186,7 +186,7 @@ export const allCommands: OSBMahojiCommand[] = [ tamesCommand, testerShopCommand, bsoLeaguesCommand, - halloweenCommand + christmasCommand ]; if (!globalConfig.isProduction && testPotatoCommand) { diff --git a/src/mahoji/commands/chop.ts b/src/mahoji/commands/chop.ts index c2376c1f6d..9a0118c882 100644 --- a/src/mahoji/commands/chop.ts +++ b/src/mahoji/commands/chop.ts @@ -145,6 +145,8 @@ export const chopCommand: OSBMahojiCommand = { let { quantity, powerchop, forestry_events, twitchers_gloves } = options; + if (log.name === 'Snowdream logs') quantity = 8; + const skills = user.skillsAsLevels; if (skills.woodcutting < log.level) { diff --git a/src/mahoji/commands/christmas.ts b/src/mahoji/commands/christmas.ts new file mode 100644 index 0000000000..fe18d3c462 --- /dev/null +++ b/src/mahoji/commands/christmas.ts @@ -0,0 +1,132 @@ +import { type CommandRunOptions, formatDuration, mentionCommand } from '@oldschoolgg/toolkit'; +import { ApplicationCommandOptionType } from 'discord.js'; + +import { Time, clamp } from 'e'; +import { SNOWDREAM_RUNES_PER_MINUTE, allChristmasEventItems, christmasDroprates } from '../../lib/christmasEvent.js'; +import type { SnoozeSpellActiveCastOptions } from '../../lib/types/minions.js'; +import { Bank, itemNameFromID } from '../../lib/util.js'; +import addSubTaskToActivityTask from '../../lib/util/addSubTaskToActivityTask.js'; +import { newChatHeadImage } from '../../lib/util/chatHeadImage.js'; +import type { OSBMahojiCommand } from '../lib/util'; + +export const christmasCommand: OSBMahojiCommand = { + name: 'christmas', + description: 'The 2024 Christmas Event', + options: [ + { + type: ApplicationCommandOptionType.Subcommand, + name: 'check', + description: 'Check your progress/information for the event', + options: [] + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'active_cast', + description: 'Send your minion on an active snooze-spell casting trip.', + options: [ + { + type: ApplicationCommandOptionType.Integer, + name: 'hours', + description: 'How many hours to do the active cast for (1-3 hours, default 1)', + required: false, + min_value: 1, + max_value: 3 + } + ] + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'drop_rates', + description: 'View the droprates of items.', + options: [] + } + ], + run: async ({ + options, + userID, + channelID + }: CommandRunOptions<{ + check?: {}; + drop_rates?: {}; + active_cast?: { + hours?: number; + }; + }>) => { + const user = await mUserFetch(userID); + + async function img(content: string) { + return newChatHeadImage({ + content, + head: 'magnaboy' + }); + } + + if (user.isIronman && !user.owns('Snowflake amulet')) { + await user.addItemsToBank({ items: new Bank().add('Snowflake amulet', 1), collectionLog: true }); + return { + files: [ + await img( + 'Take this Snowflake amulet, it will help you with the restrictions you chose to put on yourself!' + ) + ] + }; + } + + if (!user.owns('Snowdream staff')) { + return { + content: `${mentionCommand(globalClient, 'chop')}`, + files: [await img('You need a Snowdream staff! Chop some Snowdream logs and make one.')] + }; + } + if (!user.owns('Snowdream rune')) { + return { + content: `${mentionCommand(globalClient, 'runecraft')}`, + files: [await img('You have no Snowdream runes! Runecraft some or buy some.')] + }; + } + + if (options.drop_rates) { + return `**Christmas Event 2024 Droprates** +${christmasDroprates + .map(drop => { + const items = drop.items.map(itemNameFromID).join(', '); + return `**${items}**: 1 in ${drop.chancePerMinute} chance per minute (on avg: ${(drop.chancePerMinute / 60).toFixed(1)}h passive, ${(drop.chancePerMinute / 60 / 2.5).toFixed(1)}h active, ${drop.clDropRateIncrease ? `, droprate multiplied by ${drop.clDropRateIncrease} each time you get one` : ''})`; + }) + .join('\n')}`; + } + + if (options.check) { + const missingEventItems = allChristmasEventItems.filter(i => !user.cl.has(i)); + + return `**Christmas Event 2024** +You have... +**Snowdream runes:** ${user.bank.amount('Snowdream rune').toLocaleString()} +**Event Items:** ${allChristmasEventItems.filter(i => user.cl.has(i)).length}/${allChristmasEventItems.length} (Missing items: ${missingEventItems.length > 0 ? missingEventItems.slice(0, 8).map(itemNameFromID).join(', ') : 'None'}) + +View the droprates with ${mentionCommand(globalClient, 'christmas', 'drop_rates')}`; + } + + if (options.active_cast) { + let hours = clamp(options.active_cast.hours ?? 1, 1, 3); + if (Number.isNaN(hours)) hours = 1; + const duration = hours * Time.Hour; + const minutes = Math.floor(duration / Time.Minute); + const runesNeeded = Math.ceil(minutes * SNOWDREAM_RUNES_PER_MINUTE * 1.5); + const cost = new Bank().add('Snowdream rune', runesNeeded); + if (user.bank.amount('Snowdream rune') < cost.amount('Snowdream rune')) { + return `You need ${cost.amount('Snowdream rune').toLocaleString()} Snowdream runes to cast the Snooze Spell actively for ${formatDuration(duration)}.`; + } + await user.removeItemsFromBank(cost); + await addSubTaskToActivityTask({ + userID: user.id, + channelID, + duration, + type: 'SnoozeSpellActive', + hours + }); + + return `${user.minionName} is now casting the Snooze Spell actively for ${formatDuration(duration)}, removed ${cost}.`; + } + return 'Invalid options.'; + } +}; diff --git a/src/mahoji/commands/halloween.ts b/src/mahoji/commands/halloween.ts deleted file mode 100644 index 0d191394de..0000000000 --- a/src/mahoji/commands/halloween.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { CommandRunOptions } from '@oldschoolgg/toolkit'; -import { ApplicationCommandOptionType } from 'discord.js'; -import { Bank } from 'oldschooljs'; - -import { roll } from 'e'; -import type { OSBMahojiCommand } from '../lib/util'; - -export const halloweenCommand: OSBMahojiCommand = { - name: 'halloween', - description: 'The 2024 Halloween Event', - options: [ - { - type: ApplicationCommandOptionType.Subcommand, - name: 'check', - description: 'Check your progress/information for the event', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'hand_in', - description: 'Hand in your jack-o-lanterns.', - options: [] - } - ], - run: async ({ - options, - userID - }: CommandRunOptions<{ - check?: {}; - hand_in?: {}; - }>) => { - const user = await mUserFetch(userID); - - if (options.hand_in) { - if (user.bank.amount('Jack-o-lantern') < 1) { - return `You don't have any Jack-o-lanterns to hand in.`; - } - const cost = new Bank().add('Jack-o-lantern', user.bank.amount('Jack-o-lantern')); - const loot = new Bank().add('Pumpkin seed', 100); - for (let i = 0; i < cost.amount('Jack-o-lantern'); i++) { - if (roll(10)) { - loot.add('Halloween cracker'); - } - } - await user.transactItems({ itemsToAdd: loot, itemsToRemove: cost, collectionLog: true }); - const str = `${loot.has('Halloween cracker') ? '🟣 ' : ''}You handed in ${cost}, and received ${loot}!`; - return str; - } - if (options.check) { - return `**Halloween Event 2024** -The event is over! Hand in your remaining jack-o-lanterns now before this command is removed. - -You have... -**Heirloom pumpkins:** ${user.bank.amount('Heirloom pumpkin')} -**Jack-o-lanterns:** ${user.bank.amount('Jack-o-lantern')}`; - } - - return 'Invalid options.'; - } -}; diff --git a/src/tasks/minions/bso/snoozeActivity.ts b/src/tasks/minions/bso/snoozeActivity.ts new file mode 100644 index 0000000000..174580c919 --- /dev/null +++ b/src/tasks/minions/bso/snoozeActivity.ts @@ -0,0 +1,35 @@ +import { Time, roll } from 'e'; +import { Bank } from 'oldschooljs'; + +import { christmasDroprates } from '../../../lib/christmasEvent.js'; +import type { SnoozeSpellActiveCastOptions } from '../../../lib/types/minions.js'; +import { formatDuration } from '../../../lib/util.js'; +import { handleTripFinish } from '../../../lib/util/handleTripFinish'; + +export const snoozeSpellActiveTask: MinionTask = { + type: 'SnoozeSpellActive', + async run(data: SnoozeSpellActiveCastOptions) { + const { channelID, duration, userID } = data; + const user = await mUserFetch(userID); + const spellsCast = Math.floor(duration / Time.Minute); + const loot = new Bank(); + for (const drop of christmasDroprates) { + let dropRate = drop.chancePerMinute; + if (drop.clDropRateIncrease && user.cl.amount(drop.items[0]) >= 1) { + dropRate *= user.cl.amount(drop.items[0]) * drop.clDropRateIncrease; + } + dropRate = Math.ceil(dropRate / 2.5); + for (let i = 0; i < spellsCast; i++) { + if (roll(dropRate)) { + for (const item of drop.items) loot.add(item); + } + } + } + + if (loot.length > 0) { + await user.addItemsToBank({ items: loot, collectionLog: true }); + } + const str = `${user}, ${user.minionName} finished snooze spell casting for ${formatDuration(duration)}, you received ${loot}.`; + return handleTripFinish(user, channelID, str, undefined, data, null); + } +}; diff --git a/src/tasks/minions/runecraftActivity.ts b/src/tasks/minions/runecraftActivity.ts index 9fe5c1861f..d688f77a67 100644 --- a/src/tasks/minions/runecraftActivity.ts +++ b/src/tasks/minions/runecraftActivity.ts @@ -71,6 +71,11 @@ export const runecraftTask: MinionTask = { runeQuantity += bonusBlood; } + if (runeID === itemID('Snowdream rune') && user.isIronman && user.hasEquipped('Snowflake amulet')) { + runeQuantity *= 3; + str += ' You received 3x the amount of Snowdream runes from your Snowflake amulet.'; + } + const loot = new Bank({ [rune.id]: runeQuantity }); diff --git a/tests/unit/snapshots/banksnapshots.test.ts.snap b/tests/unit/snapshots/banksnapshots.test.ts.snap index 306281c468..6c8b2c0709 100644 --- a/tests/unit/snapshots/banksnapshots.test.ts.snap +++ b/tests/unit/snapshots/banksnapshots.test.ts.snap @@ -18155,6 +18155,16 @@ exports[`BSO Creatables 1`] = ` "73225": 1, }, }, + { + "cantHaveItems": undefined, + "inputItems": { + "73304": 8, + }, + "name": "Snowdream staff", + "outputItems": { + "73305": 1, + }, + }, { "cantHaveItems": undefined, "inputItems": { diff --git a/tests/unit/snapshots/clsnapshots.test.ts.snap b/tests/unit/snapshots/clsnapshots.test.ts.snap index 675cb20229..c27232fb26 100644 --- a/tests/unit/snapshots/clsnapshots.test.ts.snap +++ b/tests/unit/snapshots/clsnapshots.test.ts.snap @@ -36,6 +36,7 @@ Chompy Birds (19) Christmas 2021 (27) Christmas 2022 (15) Christmas 2023 (15) +Christmas 2024 (16) Christmas Cracker (12) Clothing Mystery Box (195) Colossal Wyrm Agility (8) @@ -44,10 +45,10 @@ Cooking (28) Corporeal Beast (8) Crafting (182) Crazy archaeologist (3) -Creatables (794) +Creatables (795) Creature Creation (7) Custom Pets (47) -Custom Pets (Discontinued) (22) +Custom Pets (Discontinued) (24) Cyclopes (8) Dagannoth Kings (10) Daily (29)