Skip to content
3 changes: 3 additions & 0 deletions apps/frontend/components/items/Items.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -81,6 +82,8 @@
<GuildBanks slug1={params.slug2} slug2={params.slug3} />
{:else if params.slug1 === 'search'}
<Search />
{:else if params.slug1 === 'search-v2'}
<SearchV2 />
{:else if params.slug1 === 'tokens'}
<Tokens />
{:else}
Expand Down
41 changes: 41 additions & 0 deletions apps/frontend/components/items/search/AllNone.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import Button from '@/shared/components/forms/Button.svelte';

let element: HTMLElement = $state(null);
let inputs = $derived(element?.parentElement.parentElement.querySelectorAll('input'));

const selectAll = () => {
for (const input of inputs) {
if (!input.checked) {
input.click();
}
}
};
const selectNone = () => {
for (const input of inputs) {
if (input.checked) {
input.click();
}
}
};
</script>

<style lang="scss">
div {
display: flex;
flex-wrap: none;
gap: var(--padding-size);

:global(button) {
padding: 0.125rem var(--padding-size);
}
span {
font-size: 90%;
}
}
</style>

<div bind:this={element}>
<Button cls="bg-success" onclick={selectAll}><span>All</span></Button>
<Button cls="bg-warn" onclick={selectNone}><span>None</span></Button>
</div>
86 changes: 86 additions & 0 deletions apps/frontend/components/items/search/Options.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script lang="ts">
import { ItemLocation } from '@/enums/item-location';
import { itemQualities, ItemQuality } from '@/enums/item-quality';
import { browserState } from '@/shared/state/browser.svelte';
import { searchState } from './state.svelte';

import AllNone from './AllNone.svelte';
import Button from '@/shared/components/forms/Button.svelte';
import GroupedCheckboxInput from '@/shared/components/forms/GroupedCheckboxInput.svelte';
import TextInput from '@/shared/components/forms/TextInput.svelte';

let searchDisabled = $derived(
browserState.current.itemsSearch.locations.length === 0 ||
browserState.current.itemsSearch.qualities.length === 0
);

const itemLocations = [
[ItemLocation.Bags, 'Bags'],
[ItemLocation.Bank, 'Bank'],
[ItemLocation.Equipped, 'Equipped'],
[ItemLocation.WarbandBank, 'Warbank'],
[ItemLocation.GuildBank, 'Guild Bank'],
];
</script>

<style lang="scss">
h4 {
display: flex;
justify-content: space-between;
}
.options {
display: flex;
flex-direction: column;
gap: calc(var(--padding-size) * 2);
padding: calc(var(--padding-size) * 2);
}
.column-options {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(var(--rows), 1fr);
}
</style>

<div class="options border">
<div class="option">
<TextInput
name="item_name"
clearButton={true}
placeholder="Item name..."
bind:value={browserState.current.itemsSearch.name}
/>
</div>

<div class="option">
<h4>Location <AllNone /></h4>
<div class="column-options" style:--rows={Math.ceil(itemLocations.length / 2)}>
{#each itemLocations as [value, text] (value)}
<GroupedCheckboxInput
name="location_{value}"
value={value.toString()}
bind:bindGroup={browserState.current.itemsSearch.locations}
>{text}</GroupedCheckboxInput
>
{/each}
</div>
</div>

<div class="option">
<h4>Quality <AllNone /></h4>
<div class="column-options" style:--rows={Math.ceil(itemQualities.length / 2)}>
{#each itemQualities as itemQuality (itemQuality)}
<GroupedCheckboxInput
name="quality_{itemQuality}"
textClass="quality{itemQuality}"
value={itemQuality.toString()}
bind:bindGroup={browserState.current.itemsSearch.qualities}
>{ItemQuality[itemQuality]}</GroupedCheckboxInput
>
{/each}
</div>
</div>

<Button cls="bg-success" disabled={searchDisabled} onclick={() => searchState.search()}
>Search</Button
>
</div>
26 changes: 26 additions & 0 deletions apps/frontend/components/items/search/Results.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import { ItemLocation } from '@/enums/item-location';
import { searchState } from './state.svelte';
import { wowthingData } from '@/shared/stores/data';
</script>

{#if searchState.inProgress}
Searching...
{:else if searchState.results !== null}
{#each searchState.results as [location, entityId, userItem]}
{@const item = wowthingData.items.items[userItem.itemId]}
<div>
<div>
{ItemLocation[location]}
</div>
<div>
{#if entityId > 0}
{entityId}
{/if}
</div>
<div>
{item.name}
</div>
</div>
{/each}
{/if}
16 changes: 16 additions & 0 deletions apps/frontend/components/items/search/Search.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import Options from './Options.svelte';
import Results from './Results.svelte';
</script>

<style lang="scss">
.wrapper {
display: flex;
gap: 1rem;
}
</style>

<div class="wrapper">
<Options />
<Results />
</div>
88 changes: 88 additions & 0 deletions apps/frontend/components/items/search/search-items.svelte.ts
Original file line number Diff line number Diff line change
@@ -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<number>();
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;
}
33 changes: 33 additions & 0 deletions apps/frontend/components/items/search/state.svelte.ts
Original file line number Diff line number Diff line change
@@ -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();
7 changes: 7 additions & 0 deletions apps/frontend/components/items/search/types.ts
Original file line number Diff line number Diff line change
@@ -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][];
};
2 changes: 2 additions & 0 deletions apps/frontend/data/achievements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const forceSupersededBy: Record<number, number> = {
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<number> = new Set<number>([
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/data/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export abstract class Constants {

static readonly items = {
petCage: 82800,
trovehuntersBounty: 274374, // Midnight Season 2
};

static readonly reputations = {
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/data/quests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export const questNameOverride: Record<number, string> = {
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<number, GlobalDailyQuest> = Object.fromEntries(
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/data/spells.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
];
17 changes: 14 additions & 3 deletions apps/frontend/data/tasks/11-midnight/12-0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading