diff --git a/apps/frontend/components/items/Items.svelte b/apps/frontend/components/items/Items.svelte index 229df4e51..73d6fefd9 100644 --- a/apps/frontend/components/items/Items.svelte +++ b/apps/frontend/components/items/Items.svelte @@ -17,6 +17,7 @@ import RowItems from './ItemsTableRowItems.svelte'; import RowUpgrades from './ItemsTableRowUpgrades.svelte'; import Search from './ItemsSearch.svelte'; + import SearchV2 from './search/Search.svelte'; import Tokens from './tokens/Tokens.svelte'; let { params }: ParamsSlugsProps = $props(); @@ -81,6 +82,8 @@ {:else if params.slug1 === 'search'} + {:else if params.slug1 === 'search-v2'} + {:else if params.slug1 === 'tokens'} {:else} diff --git a/apps/frontend/components/items/search/AllNone.svelte b/apps/frontend/components/items/search/AllNone.svelte new file mode 100644 index 000000000..4b313d293 --- /dev/null +++ b/apps/frontend/components/items/search/AllNone.svelte @@ -0,0 +1,41 @@ + + + + +
+ + +
diff --git a/apps/frontend/components/items/search/Options.svelte b/apps/frontend/components/items/search/Options.svelte new file mode 100644 index 000000000..badfa20f5 --- /dev/null +++ b/apps/frontend/components/items/search/Options.svelte @@ -0,0 +1,86 @@ + + + + +
+
+ +
+ +
+

Location

+
+ {#each itemLocations as [value, text] (value)} + {text} + {/each} +
+
+ +
+

Quality

+
+ {#each itemQualities as itemQuality (itemQuality)} + {ItemQuality[itemQuality]} + {/each} +
+
+ + +
diff --git a/apps/frontend/components/items/search/Results.svelte b/apps/frontend/components/items/search/Results.svelte new file mode 100644 index 000000000..d9a14ddeb --- /dev/null +++ b/apps/frontend/components/items/search/Results.svelte @@ -0,0 +1,26 @@ + + +{#if searchState.inProgress} + Searching... +{:else if searchState.results !== null} + {#each searchState.results as [location, entityId, userItem]} + {@const item = wowthingData.items.items[userItem.itemId]} +
+
+ {ItemLocation[location]} +
+
+ {#if entityId > 0} + {entityId} + {/if} +
+
+ {item.name} +
+
+ {/each} +{/if} diff --git a/apps/frontend/components/items/search/Search.svelte b/apps/frontend/components/items/search/Search.svelte new file mode 100644 index 000000000..d82e38bd5 --- /dev/null +++ b/apps/frontend/components/items/search/Search.svelte @@ -0,0 +1,16 @@ + + + + +
+ + +
diff --git a/apps/frontend/components/items/search/search-items.svelte.ts b/apps/frontend/components/items/search/search-items.svelte.ts new file mode 100644 index 000000000..54574439f --- /dev/null +++ b/apps/frontend/components/items/search/search-items.svelte.ts @@ -0,0 +1,88 @@ +import { browserState } from '@/shared/state/browser.svelte'; +import { wowthingData } from '@/shared/stores/data'; +import { ItemLocation } from '@/enums/item-location'; +import { userState } from '@/user-home/state/user'; +import type { SearchItemsResult } from './types'; +import { bagSlots } from '@/data/bag-slots'; + +const MAX_ITEM_IDS = 1000; + +export function searchItems(): SearchItemsResult { + console.time('searchItems'); + + const ret: SearchItemsResult = { + tooManyItems: false, + results: [], + }; + const snapshot = $state.snapshot(browserState.current.itemsSearch); + + // \p{L} = letters, \p{N} = numbers + const nameParts = snapshot.name.replace(/[^ \p{L}\p{N}]/gu, '').split(/\s+/); + + const itemIds = new Set(); + if (nameParts.length > 0) { + const nameRegexes = nameParts.map((s) => new RegExp(s, 'i')); + let itemIdCount = 0; + for (const item of Object.values(wowthingData.items.items)) { + if (nameRegexes.every((regex) => item.name.match(regex))) { + if (itemIdCount === MAX_ITEM_IDS) { + ret.tooManyItems = true; + break; + } + + itemIds.add(item.id); + itemIdCount++; + } + } + } + + const locations = snapshot.locations.map((l) => parseInt(l)); + for (const location of locations) { + switch (location) { + case ItemLocation.Bags: + case ItemLocation.Bank: + for (const character of userState.general.activeCharacters) { + for (const charItem of character.itemsByLocation[location]) { + if (!!charItem && itemIds.has(charItem.itemId)) { + ret.results.push([location, character.id, charItem]); + } + } + // check for bag/bank items + } + break; + + case ItemLocation.Equipped: + // TODO: equipped items should implement UserItem + // for (const character of userState.general.activeCharacters) { + // for (const equippedItem of Object.values(character.equippedItems || {})) { + // if (!!equippedItem && itemIds.has(equippedItem.itemId)) { + // ret.results.push([location, character.id, equippedItem]); + // } + // } + // } + break; + + case ItemLocation.WarbandBank: + for (const warbankItem of userState.general.warbankItems) { + if (itemIds.has(warbankItem.itemId)) { + ret.results.push([location, 0, warbankItem]); + } + } + break; + + case ItemLocation.GuildBank: + for (const guild of Object.values(userState.general.guildById)) { + // check for guild bank items + } + break; + } + } + + // qualities are annoying because bonusIDs exist + // const qualities = snapshot.qualities.map((q) => parseInt(q)); + + console.timeEnd('searchItems'); + console.log(ret); + + return ret; +} diff --git a/apps/frontend/components/items/search/state.svelte.ts b/apps/frontend/components/items/search/state.svelte.ts new file mode 100644 index 000000000..18fe11e6b --- /dev/null +++ b/apps/frontend/components/items/search/state.svelte.ts @@ -0,0 +1,33 @@ +import { searchItems } from './search-items.svelte'; +import type { SearchItemsResult } from './types'; + +class SearchState { + #inProgress: boolean = $state(false); + #tooManyItems: boolean = $state(false); + #results: SearchItemsResult['results'] = $state(null); + + get inProgress() { + return this.#inProgress; + } + + get tooManyItems() { + return this.#tooManyItems; + } + + get results() { + return this.#results; + } + + search() { + this.#inProgress = true; + this.#tooManyItems = false; + + const { results, tooManyItems } = searchItems(); + + this.#tooManyItems = tooManyItems; + this.#inProgress = false; + this.#results = results; + } +} + +export const searchState = new SearchState(); diff --git a/apps/frontend/components/items/search/types.ts b/apps/frontend/components/items/search/types.ts new file mode 100644 index 000000000..8bfd8a1b6 --- /dev/null +++ b/apps/frontend/components/items/search/types.ts @@ -0,0 +1,7 @@ +import type { ItemLocation } from '@/enums/item-location'; +import type { UserItem } from '@/types/shared'; + +export type SearchItemsResult = { + tooManyItems: boolean; + results: [ItemLocation, number, UserItem][]; +}; diff --git a/apps/frontend/data/achievements.ts b/apps/frontend/data/achievements.ts index 1c3fc3e83..269a16bdf 100644 --- a/apps/frontend/data/achievements.ts +++ b/apps/frontend/data/achievements.ts @@ -18,6 +18,7 @@ export const forceSupersededBy: Record = { 5327: 5328, // In Service of the Alliance -> Veteran of the Alliance 13701: 13702, // Battlefield Brawler -> Battlefield Tactician 13702: 13703, // Battlefield Tactician -> Battlefield Master + 63434: 63435, // Buddy System VII: Valeera -> Buddy System VIII: Valeera }; export const forceShowCriteriaTree: Set = new Set([ @@ -527,6 +528,7 @@ export const extraCategories: ExtraAchievementCategory[] = [ 61222, // Tour of Duty: Zul'Aman 61223, // Tour of Duty: Harandar 61224, // Tour of Duty: Voidstorm + 63167, // Tour of Duty: The Coiled Isle ], }, null, diff --git a/apps/frontend/data/constants.ts b/apps/frontend/data/constants.ts index 1935ab57e..ebc7022f3 100644 --- a/apps/frontend/data/constants.ts +++ b/apps/frontend/data/constants.ts @@ -28,6 +28,7 @@ export abstract class Constants { static readonly items = { petCage: 82800, + trovehuntersBounty: 274374, // Midnight Season 2 }; static readonly reputations = { diff --git a/apps/frontend/data/quests.ts b/apps/frontend/data/quests.ts index 776d331af..d26a7154f 100644 --- a/apps/frontend/data/quests.ts +++ b/apps/frontend/data/quests.ts @@ -60,6 +60,9 @@ export const questNameOverride: Record = { 94743: 'Voidstorm WQs', // Special Assignment: Precision Excision 94865: "Zul'Aman WQs", // Special Assignment: What Remains of a Temple Broken 94866: "Zul'Aman WQs", // Special Assignment: Ours Once More! + 96492: 'Coiled Isle WQs', // Special Assignment: Demand and Supply + 96029: 'Coiled Isle WQs', // Special Assignment: Face the Swarm + 96307: 'Coiled Isle WQs', // Special Assignment: Wraith Wrath }; export const globalDailyQuests: Record = Object.fromEntries( diff --git a/apps/frontend/data/spells.ts b/apps/frontend/data/spells.ts index ffa6399cc..d46b1e5b6 100644 --- a/apps/frontend/data/spells.ts +++ b/apps/frontend/data/spells.ts @@ -43,5 +43,5 @@ export const durationAuras: [number, string, boolean?][] = [ export const staticAuras: [number, string][] = [ [306715, 'XP gain disabled'], [1246363, "Delver's Bounty"], - [1254631, "Trovehunter's Bounty"], + [1293799, "Trovehunter's Bounty"], ]; diff --git a/apps/frontend/data/tasks/11-midnight/12-0.ts b/apps/frontend/data/tasks/11-midnight/12-0.ts index 50944a771..317d627ae 100644 --- a/apps/frontend/data/tasks/11-midnight/12-0.ts +++ b/apps/frontend/data/tasks/11-midnight/12-0.ts @@ -47,9 +47,19 @@ export const specialAssignmentFunc = (unlocks: number[], index: number, isQuest: .map((worldQuest, index) => [worldQuest.questId, index]) ); - const activeQuests = unlocks - .filter((questId) => questIdIndexes[questId]) - .map((questId) => [questIdIndexes[questId], questId]); + const activeQuests: [number, number][] = []; + for (const unlockQuestId of unlocks) { + if (questIdIndexes[unlockQuestId]) { + activeQuests.push([questIdIndexes[unlockQuestId], unlockQuestId]); + } else { + // Midnight 12.1 world quests have the actual quest as active, not the unlock + const actualQuestId = specialAssignmentUnlockToQuest[unlockQuestId]; + if (actualQuestId && questIdIndexes[actualQuestId]) { + activeQuests.push([questIdIndexes[actualQuestId], unlockQuestId]); + } + } + } + activeQuests.sort((a, b) => a[0] - b[0]); if (activeQuests[index]) { @@ -114,6 +124,7 @@ export const midChores12_0: Task = { 95843, // Midnight: Ritual Sites 93889, // Midnight: Saltheril's Soiree 93892, // Midnight: Stormarion Assault + 98232, // Midnight: Vaults of Atal'Utek 95842, // Midnight: Void Assaults 93913, // Midnight: World Boss 93766, // Midnight: World Quests diff --git a/apps/frontend/data/tasks/11-midnight/12-1.ts b/apps/frontend/data/tasks/11-midnight/12-1.ts index 47e8bb150..4f9b9c910 100644 --- a/apps/frontend/data/tasks/11-midnight/12-1.ts +++ b/apps/frontend/data/tasks/11-midnight/12-1.ts @@ -1,8 +1,9 @@ import { iconLibrary } from '@/shared/icons'; import { DbResetType } from '@/shared/stores/db/enums'; -import type { Chore, Task } from '@/types/tasks'; +import { nthActiveQuest } from '@/utils/tasks/nth-active-quest'; +import type { Task } from '@/types/tasks'; -import { specialAssignmentExpiry, specialAssignmentFunc } from './12-0'; +import { specialAssignmentFunc } from './12-0'; const specialAssignmentUnlocks = [ 96492, // Special Assignment: Demand and Supply @@ -10,6 +11,15 @@ const specialAssignmentUnlocks = [ 96307, // Special Assignment: Wraith Wrath ]; +const vaultDailies = [ + 96639, // Patrolling the Temple + 96641, // Relentless Strikes + 96642, // Decisive Incursions + 96643, // From Whence it Came + 98419, // Shoulder to Shoulder + 98420, // What's Out There? +]; + export const midChores12_1: Task = { key: 'midChores12_1', name: '[Mid] 12.1.x', @@ -17,12 +27,60 @@ export const midChores12_1: Task = { showSeparate: true, chores: [ { - key: 'midSpecial1', - name: 'Special Assignment 1', - icon: iconLibrary.mdiNumeric1CircleOutline, + key: 'trailing', + name: "Trailing Xal'atath", + icon: iconLibrary.mdiFootPrint, + minimumLevel: 90, + questReset: DbResetType.Weekly, + questIds: [98172], + }, + { + key: 'curseSurge', + name: 'Curse Surges', + icon: iconLibrary.gameCursedStar, + minimumLevel: 90, + questReset: DbResetType.Weekly, + questIds: [96995], + }, + { + key: 'purgingVaults', + name: 'Purging the Vaults', + icon: iconLibrary.mdiSafeSquareOutline, + minimumLevel: 90, + questReset: DbResetType.Weekly, + questIds: [95520], + }, + { + key: 'vaultDailies', + name: 'Vault Dailies', + icon: iconLibrary.emojiDoubleExclamationMark, + minimumLevel: 90, + alwaysStarted: true, + subChoresAnyOrder: true, + questReset: DbResetType.Daily, + questResetForced: true, + subChores: [ + { + key: 'daily1', + name: 'Daily 1', + showQuestName: true, + questIds: nthActiveQuest(vaultDailies, 0), + }, + { + key: 'daily2', + name: 'Daily 2', + showQuestName: true, + questIds: nthActiveQuest(vaultDailies, 1), + }, + ], + }, + { + key: 'specialAssignment', + name: 'Special Assignment', + icon: ':island:', + minimumLevel: 90, showQuestName: true, - questReset: DbResetType.Custom, - customExpiryFunc: specialAssignmentExpiry, + questReset: DbResetType.Weekly, subChores: [ { key: 'unlock', @@ -30,15 +88,14 @@ export const midChores12_1: Task = { alwaysStarted: true, overrideNeed: 3, questIds: specialAssignmentFunc(specialAssignmentUnlocks, 0, false), - customExpiryFunc: specialAssignmentExpiry, }, { key: 'assignment', name: 'Assignment', alwaysStarted: true, showQuestName: true, + overrideNeed: 1, questIds: specialAssignmentFunc(specialAssignmentUnlocks, 0, true), - customExpiryFunc: specialAssignmentExpiry, }, ], }, diff --git a/apps/frontend/data/tasks/11-midnight/delves.ts b/apps/frontend/data/tasks/11-midnight/delves.ts index ba657adcb..ba186221d 100644 --- a/apps/frontend/data/tasks/11-midnight/delves.ts +++ b/apps/frontend/data/tasks/11-midnight/delves.ts @@ -4,6 +4,7 @@ import { timeState } from '@/shared/state/time.svelte'; import { DbResetType } from '@/shared/stores/db/enums'; import { getNextWeeklyResetFromTime } from '@/utils/get-next-reset'; import type { Task } from '@/types/tasks'; +import { Constants } from '@/data/constants'; export const midDelves: Task = { key: 'midDelves', @@ -62,11 +63,10 @@ export const midDelves: Task = { null, { key: 'bounty', - name: '{item:252415}', // Trovehunter's Bounty [Season 1] + name: `{item:${Constants.items.trovehuntersBounty}}`, // Trovehunter's Bounty [Season 2] icon: iconLibrary.gameTreasureMap, minimumLevel: 90, alwaysStarted: true, - subChoresAnyOrder: true, questReset: DbResetType.Weekly, questResetForced: true, questIds: [86371], diff --git a/apps/frontend/enums/item-quality.ts b/apps/frontend/enums/item-quality.ts index 4fb9e7b09..06da02a92 100644 --- a/apps/frontend/enums/item-quality.ts +++ b/apps/frontend/enums/item-quality.ts @@ -9,3 +9,7 @@ export enum ItemQuality { Heirloom = 7, Token = 8, } + +export const itemQualities = Object.values(ItemQuality).filter( + (value) => typeof value === 'number' && value !== ItemQuality.Token +); diff --git a/apps/frontend/shared/icons/library.ts b/apps/frontend/shared/icons/library.ts index f85aec2ac..51bf62918 100644 --- a/apps/frontend/shared/icons/library.ts +++ b/apps/frontend/shared/icons/library.ts @@ -1,6 +1,7 @@ export { default as carbonScatterMatrix } from '~icons/carbon/scatter-matrix'; export { default as emojiConstruction } from '~icons/emojione-monotone/construction'; +export { default as emojiDoubleExclamationMark } from '~icons/emojione-monotone/double-exclamation-mark'; export { default as emojiWorldMap } from '~icons/emojione-monotone/world-map'; export { default as emojiZzz } from '~icons/emojione-monotone/zzz'; @@ -37,6 +38,7 @@ export { default as gameCompanionCube } from '~icons/game-icons/companion-cube'; export { default as gameCrossbow } from '~icons/game-icons/crossbow'; export { default as gameCrossedSwords } from '~icons/game-icons/crossed-swords'; export { default as gameCrownedSkull } from '~icons/game-icons/crowned-skull'; +export { default as gameCursedStar } from '~icons/game-icons/cursed-star'; export { default as gameCurvyKnife } from '~icons/game-icons/curvy-knife'; export { default as gameCutDiamond } from '~icons/game-icons/cut-diamond'; export { default as gameDeathSkull } from '~icons/game-icons/death-skull'; @@ -141,6 +143,7 @@ export { default as mdiDuck } from '~icons/mdi/duck'; export { default as mdiEmoticonDevilOutline } from '~icons/mdi/emoticon-devil-outline'; export { default as mdiExclamationThick } from '~icons/mdi/exclamation-thick'; export { default as mdiFire } from '~icons/mdi/fire'; +export { default as mdiFootPrint } from '~icons/mdi/foot-print'; export { default as mdiFormatListCheckbox } from '~icons/mdi/format-list-checkbox'; export { default as mdiGiftOutline } from '~icons/mdi/gift-outline'; export { default as mdiGhost } from '~icons/mdi/ghost'; @@ -148,6 +151,7 @@ export { default as mdiHammerWrench } from '~icons/mdi/hammer-wrench'; export { default as mdiHomeOutline } from '~icons/mdi/home-outline'; export { default as mdiHourglass } from '~icons/mdi/hourglass'; export { default as mdiImageBrokenVariant } from '~icons/mdi/image-broken-variant'; +export { default as mdiIsland } from '~icons/mdi/island'; export { default as mdiLetterC } from '~icons/mdi/alpha-c-circle-outline'; export { default as mdiLetterD } from '~icons/mdi/alpha-d-circle-outline'; export { default as mdiLetterF } from '~icons/mdi/alpha-f-circle-outline'; diff --git a/apps/frontend/shared/icons/ui.ts b/apps/frontend/shared/icons/ui.ts index b2c43bd52..5657caa40 100644 --- a/apps/frontend/shared/icons/ui.ts +++ b/apps/frontend/shared/icons/ui.ts @@ -40,6 +40,7 @@ export const aliasedIcons = { cloud: iconLibrary.notoCloud, devil: iconLibrary.mdiEmoticonDevilOutline, ice: iconLibrary.notoIce, + island: iconLibrary.mdiIsland, pinkHeart: iconLibrary.notoPinkHeart, planet: iconLibrary.mdiPlanet, rose: iconLibrary.notoRose, diff --git a/apps/frontend/shared/state/browser.svelte.ts b/apps/frontend/shared/state/browser.svelte.ts index 29f278283..1fa1d106e 100644 --- a/apps/frontend/shared/state/browser.svelte.ts +++ b/apps/frontend/shared/state/browser.svelte.ts @@ -60,6 +60,11 @@ interface BrowserStateIdk { itemLevelValue: number; minimumBagSize: number; }; + itemsSearch: { + name: string; + locations: string[]; + qualities: string[]; + }; journal: { filtersExpanded: boolean; @@ -220,6 +225,11 @@ const initialState: BrowserStateIdk = { itemLevelValue: 0, minimumBagSize: 0, }, + itemsSearch: { + name: '', + locations: ['1', '2', '4', '5', '100'], + qualities: ['0', '1', '2', '3', '4', '5', '6', '7'], + }, journal: { filtersExpanded: false, diff --git a/apps/frontend/user-home/state/user/derived.svelte.ts b/apps/frontend/user-home/state/user/derived.svelte.ts index 39316d2e8..6340938cc 100644 --- a/apps/frontend/user-home/state/user/derived.svelte.ts +++ b/apps/frontend/user-home/state/user/derived.svelte.ts @@ -713,6 +713,9 @@ export class DataUserDerived { if (chore.showQuestName || chore.subChores?.length > 0) { charChore.name = charChore.quest?.name; + if (!charChore.name && questIds.length === 1) { + charChore.name = wowthingData.static.questNameById.get(questIds[0]); + } } else if (questNameOverride[charChore.quest?.id]) { charChore.name = questNameOverride[charChore.quest.id]; } else if (questIds.length === 1 && questNameOverride[questIds[0]]) { diff --git a/apps/frontend/utils/tasks/nth-active-quest.ts b/apps/frontend/utils/tasks/nth-active-quest.ts new file mode 100644 index 000000000..a89a93adf --- /dev/null +++ b/apps/frontend/utils/tasks/nth-active-quest.ts @@ -0,0 +1,27 @@ +import { timeState } from '@/shared/state/time.svelte'; +import { dynamicDataStore } from '@/user-home/stores/dynamicData'; +import type { Character } from '@/types/character'; +import type { Chore } from '@/types/tasks'; + +export const nthActiveQuest = (questIds: number[], index: number) => { + return (char: Character, chore: Chore) => { + const now = timeState.slowTime; + const allWorldQuests = dynamicDataStore.getCachedQuests(char.region); + const questIdIndexes = Object.fromEntries( + allWorldQuests + .filter((worldQuest) => worldQuest.expires > now) + .map((worldQuest, index) => [worldQuest.questId, index]) + ); + + const activeQuests = questIds + .filter((questId) => questIdIndexes[questId]) + .map((questId) => [questIdIndexes[questId], questId]); + activeQuests.sort((a, b) => a[0] - b[0]); + + if (activeQuests[index]) { + return [activeQuests[index][1]]; + } else { + return questIds; + } + }; +};