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;
+ }
+ };
+};