From 28acc8ac9e9ca4b2d758c5b40abc6001d0b10352 Mon Sep 17 00:00:00 2001 From: Freddie Date: Mon, 13 Apr 2026 19:02:30 -0700 Subject: [PATCH 01/24] Map Midnight quality 1/2 to TWW quality 2/3 --- apps/frontend/shared/components/parsed-text/ParsedText.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/shared/components/parsed-text/ParsedText.svelte b/apps/frontend/shared/components/parsed-text/ParsedText.svelte index 01625591b..91993d6c6 100644 --- a/apps/frontend/shared/components/parsed-text/ParsedText.svelte +++ b/apps/frontend/shared/components/parsed-text/ParsedText.svelte @@ -168,7 +168,7 @@ if (item) { // Midnight qualities are 13 and 14 for some reason, pretend they're normal if (item.craftingQuality) { - return `${item.name} `; + return `${item.name} `; } else { return `${item.name}`; } From 616d6e3a264492dcd01889397de4f06d09862c81 Mon Sep 17 00:00:00 2001 From: Freddie Date: Mon, 13 Apr 2026 19:03:00 -0700 Subject: [PATCH 02/24] Handle "&" for AND and "|" for OR in character filters --- .../shared/utils/boolean-parser/index.ts | 17 +++++++++-------- .../utils/characters/use-character-filter.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/frontend/shared/utils/boolean-parser/index.ts b/apps/frontend/shared/utils/boolean-parser/index.ts index 095076c31..f8c2917e5 100644 --- a/apps/frontend/shared/utils/boolean-parser/index.ts +++ b/apps/frontend/shared/utils/boolean-parser/index.ts @@ -38,10 +38,10 @@ function injectOperatorBetweenTerms(searchPhrase: string): string { // replace all spaces with ' AND ', then remove any extra ANDs searchPhrase = searchPhrase.replace(/ /gi, ' AND '); - searchPhrase = searchPhrase.replace(/ AND AND AND /gi, ' AND '); - searchPhrase = searchPhrase.replace(/ AND OR AND /gi, ' OR '); - searchPhrase = searchPhrase.replace(/\( AND /gi, '('); - searchPhrase = searchPhrase.replace(/ AND \)/gi, ')'); + searchPhrase = searchPhrase.replace(/ (?:AND|\&) (?:AND|\&) (?:AND|\&) /gi, ' AND '); + searchPhrase = searchPhrase.replace(/ (?:AND|\&) (?:OR|\|) (?:AND|\&) /gi, ' OR '); + searchPhrase = searchPhrase.replace(/\( (?:AND|\&) /gi, '('); + searchPhrase = searchPhrase.replace(/ (?:AND|\&) \)/gi, ')'); return searchPhrase; } @@ -104,13 +104,13 @@ function _parseBooleanQuery(searchPhrase: string): string[][] { // Split the phrase on the term 'OR', but don't do this on 'OR' that's in // between brackets. EX: a OR (b OR c) should not parse the `OR` in between b // and c. - const ors = splitRoot('OR', searchPhrase); + const ors = splitRoot(/ (?:OR|\|) /, 'OR', searchPhrase); // Each parsed string returns a parsed array in this map function. const orPath = ors.map(function (andQuery) { // Split on the word 'AND'. Yet again, don't split `AND` that's written in // between brackets. We'll parse those later recursively. - const ands = splitRoot('AND', andQuery); + const ands = splitRoot(/ (?:AND|\&) /, 'AND', andQuery); // All nested parsed queries will be stored in `nestedPaths`. // Nested means 'in between brackets'. @@ -125,6 +125,7 @@ function _parseBooleanQuery(searchPhrase: string): string[][] { for (const andString of ands) { // If the string contains brackets, parse it recursively, and add it to // `nestedPaths`. + if (containsBrackets(andString)) { nestedPaths.push(_parseBooleanQuery(andString)); } @@ -300,8 +301,8 @@ function containsBrackets(str: string): boolean { // folling string from the `split` results. And stop doing that when we counted // as many opening brackets as closing brackets. Then append that string to the // results as a single string. -function splitRoot(splitTerm: string, phrase: string): string[] { - const termSplit = phrase.split(' ' + splitTerm + ' '); +function splitRoot(splitRe: RegExp, splitTerm: string, phrase: string): string[] { + const termSplit = phrase.split(splitRe); const result: string[] = []; let tempNested: string[] = []; for (let i = 0; i < termSplit.length; i++) { diff --git a/apps/frontend/utils/characters/use-character-filter.ts b/apps/frontend/utils/characters/use-character-filter.ts index 4b4316012..4a7100569 100644 --- a/apps/frontend/utils/characters/use-character-filter.ts +++ b/apps/frontend/utils/characters/use-character-filter.ts @@ -34,7 +34,7 @@ export function useCharacterFilter( partArrays = _cache[filterLower] = []; } } - // console.log(char.name, partArrays) + // console.log(char.name, partArrays); if (partArrays.length === 0) { return true; From 2912332dd80ef8e05eaa6036b3c052ef65818106 Mon Sep 17 00:00:00 2001 From: Freddie Date: Mon, 13 Apr 2026 19:04:28 -0700 Subject: [PATCH 03/24] Improve gold history performance by reusing more DateTimes --- .../user-home/components/history/Gold.svelte | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/apps/frontend/user-home/components/history/Gold.svelte b/apps/frontend/user-home/components/history/Gold.svelte index 3b2b7a5e8..00f9d39a7 100644 --- a/apps/frontend/user-home/components/history/Gold.svelte +++ b/apps/frontend/user-home/components/history/Gold.svelte @@ -50,8 +50,9 @@ x: DateTime; y: number; }; + type DateTimeTuple = [DateTime, number]; - let chart: Chart<'line', DateTimePoint[]>; + let chart: Chart<'line', DateTimeTuple[]>; let ready: boolean; $: { if (ready) { @@ -82,7 +83,7 @@ chart.destroy(); } - const data: ChartData<'line', DateTimePoint[]> = { + const data: ChartData<'line', DateTimeTuple[]> = { datasets: [], }; const stacked = historyState.chartType === 'area-stacked'; @@ -105,6 +106,7 @@ const pointMap: Record = {}; let minTime: DateTime = DateTime.now().minus({ years: 10 }); + let minTimeIso = minTime.toUTC().toISO(); if (historyState.timeFrame === '1week') { minTime = DateTime.now().minus({ weeks: 1 }); } else if (historyState.timeFrame === '1month') { @@ -119,6 +121,7 @@ let firstRealmId = -1; const timeCache: Record = {}; + const tempKeyCache: Record = {}; for (let realmIndex = 0; realmIndex < realms.length; realmIndex++) { const [realmName, realmId] = realms[realmIndex]; if (firstRealmId === -1 && realmId > 0) { @@ -130,18 +133,20 @@ const color = colors[(realmIndex + 1) * 2]; - let points: DateTimePoint[]; + let points: DateTimeTuple[]; if (historyState.interval === 'hour') { + // it's faster to compare the time string than parse->compare points = userHistoryData.gold[realmId] - .map((point) => ({ - x: parseApiTime(point[0]), - y: point[1], - })) - .filter(({ x }) => x >= minTime); + // .filter(([x]) => x >= minTimeIso) + .map(([pointTime, pointValue]) => { + const parsedTime = (timeCache[pointTime] ||= parseApiTime(pointTime)); + return [parsedTime, pointValue] as DateTimeTuple; + }) + .filter(([x]) => x >= minTime); } else { const temp: Record = {}; for (const [time, value] of userHistoryData.gold[realmId]) { - let fakeTime: DateTime = timeCache[time]; + let fakeTime = timeCache[time]; if (fakeTime === undefined) { const parsedTime = parseApiTime(time); if (historyState.interval === 'day') { @@ -199,22 +204,21 @@ } if (fakeTime >= minTime) { - // console.log(fakeTime.toISODate(), fakeTime.toISOTime()) - temp[`${fakeTime.toISODate()} ${fakeTime.toISOTime()}`] = [fakeTime, value]; + const tempKey = (tempKeyCache[time] ||= + `${fakeTime.toISODate()} ${fakeTime.toISOTime()}`); + temp[tempKey] = [fakeTime, value]; } } - points = sortBy(Object.entries(temp), ([date]) => date).map( - ([, [time, value]]) => ({ - x: time, - y: value, - }) - ); + points = sortBy(Object.entries(temp), ([date]) => date).map(([, [time, value]]) => [ + time, + value, + ]); } for (const point of points) { - if (point.x instanceof DateTime) { - pointMap[point.x.toUnixInteger()] = point.x; + if (point[0] instanceof DateTime) { + pointMap[point[0].toUnixInteger()] = point[0]; } } @@ -232,8 +236,8 @@ // Pad out each dataset with data for all time periods console.time('redrawChart.datasets'); - const smalls: Record = {}; - const totals: Record = {}; + const smalls: Record = {}; + const totals: Record = {}; const allPoints: [number, DateTime][] = Object.entries(pointMap).map(([a, b]) => [ parseInt(a), @@ -242,13 +246,10 @@ allPoints.sort(); for (const dataset of data.datasets) { - const oldMap: Record = Object.fromEntries( - dataset.data.map((dataPoint: DateTimePoint) => [ - dataPoint.x.toUnixInteger(), - dataPoint, - ]) + const oldMap: Record = Object.fromEntries( + dataset.data.map((dataPoint) => [dataPoint[0].toUnixInteger(), dataPoint]) ); - const newData: DateTimePoint[] = []; + const newData: DateTimeTuple[] = []; for (let pointIndex = 0; pointIndex < allPoints.length; pointIndex++) { const [ts, dateTime] = allPoints[pointIndex]; @@ -256,22 +257,21 @@ newData.push(oldMap[ts]); } else { if (pointIndex === 0) { - newData.push({ x: dateTime, y: 0 }); + newData.push([dateTime, 0]); } else { - newData.push({ x: dateTime, y: newData[pointIndex - 1].y }); + newData.push([dateTime, newData[pointIndex - 1][1]]); } } if (totals[ts] === undefined) { - smalls[ts] = { x: dateTime, y: 0 }; - totals[ts] = { x: dateTime, y: 0 }; + totals[ts] = smalls[ts] = [dateTime, 0]; } - const newValue = newData[pointIndex].y; - totals[ts].y += newValue; + const newValue = newData[pointIndex][1]; + totals[ts][1] += newValue; if (historyState.tooltipCombineSmall && newValue < 10000) { - smalls[ts].y += newValue; + smalls[ts][1] += newValue; } } @@ -280,16 +280,17 @@ console.timeEnd('redrawChart.datasets'); console.time('redrawChart.sort'); - data.datasets.sort((a, b) => b.data[b.data.length - 1].y - a.data[a.data.length - 1].y); + data.datasets.sort((a, b) => b.data[b.data.length - 1][1] - a.data[a.data.length - 1][1]); console.timeEnd('redrawChart.sort'); console.time('redrawChart.line'); if (historyState.chartType === 'line' && firstRealmId >= 0) { if (historyState.tooltipCombineSmall) { - const anyUseful = Object.values(smalls).filter((value) => value.y > 0).length > 0; + const anyUseful = + Object.values(smalls).filter(([, pointValue]) => pointValue > 0).length > 0; if (anyUseful) { - const smallPoints = sortBy(Object.values(smalls), (point: DateTimePoint) => - point.x.toUnixInteger() + const smallPoints = sortBy(Object.values(smalls), ([pointTime]) => + pointTime.toUnixInteger() ); data.datasets.push({ @@ -304,7 +305,9 @@ } } - const totalPoints = sortBy(Object.values(totals), (point) => point.x.toUnixInteger()); + const totalPoints = sortBy(Object.values(totals), ([pointTime]) => + pointTime.toUnixInteger() + ); data.datasets.push({ backgroundColor: colors[0], From 3ab68fd15026d9d07d3c9df57502fee5c9e2647c Mon Sep 17 00:00:00 2001 From: Freddie Date: Mon, 13 Apr 2026 23:03:02 -0700 Subject: [PATCH 04/24] Check for completed chores before in progress, fixes account-wide again --- .../user-home/state/user/derived.svelte.ts | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/apps/frontend/user-home/state/user/derived.svelte.ts b/apps/frontend/user-home/state/user/derived.svelte.ts index 966fa393a..4631b29cd 100644 --- a/apps/frontend/user-home/state/user/derived.svelte.ts +++ b/apps/frontend/user-home/state/user/derived.svelte.ts @@ -534,28 +534,6 @@ export class DataUserDerived { } for (const questId of questIds) { - // is the quest in progress? - const questProgress = characterQuests?.progressQuestByKey?.get(`q${questId}`); - if ( - questProgress && - (!resetForced || - questProgress.expires > timeState.slowTime.toUnixInteger() || - expiresAt > timeState.slowTime) - ) { - charChore.quest = questProgress; - charChore.quest.expires ||= expiresAt.toUnixInteger(); - charChore.status = questProgress.status; - - if ( - questProgress.status === QuestStatus.InProgress && - questProgress.objectives?.length > 0 - ) { - charChore.statusTexts = this.getObjectivesText(questProgress.objectives); - } - - break; - } - // is the quest completed? const questCompleted = chore.accountWide ? accountWeeklyQuestIds.has(questId) @@ -577,6 +555,26 @@ export class DataUserDerived { break; } } + + // is the quest in progress? + const questProgress = characterQuests?.progressQuestByKey?.get(`q${questId}`); + if ( + questProgress && + (!resetForced || + questProgress.expires > timeState.slowTime.toUnixInteger() || + expiresAt > timeState.slowTime) + ) { + charChore.quest = questProgress; + charChore.quest.expires ||= expiresAt.toUnixInteger(); + charChore.status = questProgress.status; + + if ( + questProgress.status === QuestStatus.InProgress && + questProgress.objectives?.length > 0 + ) { + charChore.statusTexts = this.getObjectivesText(questProgress.objectives); + } + } } } else if (chore.subChores) { // sub-chores need to be completed in order From 96d35840c57f5979f5ee0fab26898e1d57a2b28c Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 15 Apr 2026 19:07:43 -0700 Subject: [PATCH 05/24] Janky hacks for Nazjatar friendship display --- .../reputation/TooltipReputation.svelte | 26 +++++++++++++++---- .../reputation/{delve-hacks.ts => hacks.ts} | 17 ++++++++++++ apps/frontend/data/constants.ts | 2 ++ .../components/reputations/TableCell.svelte | 13 +++++++++- .../components/reputations/get-renown-data.ts | 2 +- 5 files changed, 53 insertions(+), 7 deletions(-) rename apps/frontend/components/tooltips/reputation/{delve-hacks.ts => hacks.ts} (65%) diff --git a/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte b/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte index 6b0b305ab..2eb9f1e7e 100644 --- a/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte +++ b/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte @@ -8,7 +8,7 @@ } from '@/shared/stores/static/types'; import type { Character, CharacterReputationParagon } from '@/types'; import type { ManualDataReputationSet } from '@/types/data/manual'; - import { brannHack, valeeraHack } from './delve-hacks'; + import { brannHack, nazjatarHack, valeeraHack } from './hacks'; import RenownTooltip from './TooltipReputationRenown.svelte'; import WowthingImage from '@/shared/components/images/sources/WowthingImage.svelte'; @@ -61,6 +61,16 @@ minValue = tiers.minValues[i - 9]; } } + } else if (Constants.reputations.nazjatarFriends.includes(dataRep.id)) { + const foundBase = Math.floor((foundIndex + 1) / 5); + const indexBase = Math.floor((i + 1) / 5); + if (i % 5 !== 4 && (foundIndex === -1 || foundBase !== indexBase)) { + continue; + } else { + if (foundIndex === -1 || indexBase > foundBase + 1) { + minValue = tiers.minValues[i - 4]; + } + } } reps.push({ @@ -83,12 +93,18 @@ badCount++; } else if ( dataRep.id === Constants.reputations.delveBrann || - dataRep.id === Constants.reputations.delveValeera + dataRep.id === Constants.reputations.delveValeera || + Constants.reputations.nazjatarFriends.includes(dataRep.id) ) { - const levelMatch = reps[i].name.match(/(\d\d\d?)/); + const levelMatch = reps[i].name.match(/ (\d{1,3})/); if (levelMatch) { - reps[i].cls = - `reputation${dataRep.id === Constants.reputations.delveBrann ? brannHack(levelMatch[1]) : valeeraHack(levelMatch[1])}`; + if (dataRep.id === Constants.reputations.delveBrann) { + reps[i].cls = `reputation${brannHack(levelMatch[1])}`; + } else if (dataRep.id === Constants.reputations.delveValeera) { + reps[i].cls = `reputation${valeeraHack(levelMatch[1])}`; + } else { + reps[i].cls = `reputation${nazjatarHack(reps[i].name)}`; + } } } else if (i >= setClass) { reps[i].cls = `reputation${start - i + 1}`; diff --git a/apps/frontend/components/tooltips/reputation/delve-hacks.ts b/apps/frontend/components/tooltips/reputation/hacks.ts similarity index 65% rename from apps/frontend/components/tooltips/reputation/delve-hacks.ts rename to apps/frontend/components/tooltips/reputation/hacks.ts index d2ab3fa55..cfc478a1a 100644 --- a/apps/frontend/components/tooltips/reputation/delve-hacks.ts +++ b/apps/frontend/components/tooltips/reputation/hacks.ts @@ -1,3 +1,20 @@ +export function nazjatarHack(levelString: string): number { + const level = parseInt(levelString.split(' ')[1]); + if (level >= 26) { + return 1; + } else if (level >= 21) { + return 2; + } else if (level >= 16) { + return 3; + } else if (level >= 11) { + return 4; + } else if (level >= 6) { + return 5; + } else { + return 6; + } +} + export function brannHack(levelString: string): number { const level = parseInt(levelString); if (level >= 91) { diff --git a/apps/frontend/data/constants.ts b/apps/frontend/data/constants.ts index 8acd15a80..b561ae57d 100644 --- a/apps/frontend/data/constants.ts +++ b/apps/frontend/data/constants.ts @@ -31,6 +31,8 @@ export abstract class Constants { }; static readonly reputations = { + nazjatarFriends: [2377, 2389, 2376, 2388, 2375, 2390], + artisansConsortium: 2544, loammNiffen: 2564, delveBrann: 2640, diff --git a/apps/frontend/user-home/components/reputations/TableCell.svelte b/apps/frontend/user-home/components/reputations/TableCell.svelte index dde308617..c3b8f12dc 100644 --- a/apps/frontend/user-home/components/reputations/TableCell.svelte +++ b/apps/frontend/user-home/components/reputations/TableCell.svelte @@ -1,4 +1,6 @@ diff --git a/apps/frontend/components/tooltips/reputation-header/TooltipReputationHeaderEntry.svelte b/apps/frontend/components/tooltips/reputation-header/TooltipReputationHeaderEntry.svelte index 8b4403c90..3f40416e4 100644 --- a/apps/frontend/components/tooltips/reputation-header/TooltipReputationHeaderEntry.svelte +++ b/apps/frontend/components/tooltips/reputation-header/TooltipReputationHeaderEntry.svelte @@ -25,7 +25,8 @@ !set.paragon ? 0 : userState.general.activeCharacters.reduce( - (a: number, b: Character) => a + (b.paragons?.[reputation.id]?.received ?? 0), + (a: number, b: Character) => + a + Math.max(0, b.paragons?.[reputation.id]?.received ?? 0), 0 ) ); diff --git a/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte b/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte index 2eb9f1e7e..873d2899a 100644 --- a/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte +++ b/apps/frontend/components/tooltips/reputation/TooltipReputation.svelte @@ -67,7 +67,7 @@ if (i % 5 !== 4 && (foundIndex === -1 || foundBase !== indexBase)) { continue; } else { - if (foundIndex === -1 || indexBase > foundBase + 1) { + if (foundIndex === -1 || indexBase > foundBase) { minValue = tiers.minValues[i - 4]; } } diff --git a/apps/frontend/components/tooltips/reputation/hacks.ts b/apps/frontend/components/tooltips/reputation/hacks.ts index cfc478a1a..1141a32d1 100644 --- a/apps/frontend/components/tooltips/reputation/hacks.ts +++ b/apps/frontend/components/tooltips/reputation/hacks.ts @@ -1,18 +1,6 @@ export function nazjatarHack(levelString: string): number { const level = parseInt(levelString.split(' ')[1]); - if (level >= 26) { - return 1; - } else if (level >= 21) { - return 2; - } else if (level >= 16) { - return 3; - } else if (level >= 11) { - return 4; - } else if (level >= 6) { - return 5; - } else { - return 6; - } + return level === 30 ? 1 : 7 - Math.max(1, Math.floor(level / 5)); } export function brannHack(levelString: string): number { From cd0d59ff01a1961dea218cfbe6d8fe917d039089 Mon Sep 17 00:00:00 2001 From: Freddie Date: Fri, 17 Apr 2026 16:45:27 -0700 Subject: [PATCH 07/24] Add a nazjatar task --- .../data/tasks/07-battle-for-azeroth/index.ts | 3 ++ .../tasks/07-battle-for-azeroth/nazjatar.ts | 38 +++++++++++++++++++ apps/frontend/data/tasks/task-list.ts | 2 + apps/frontend/shared/icons/library.ts | 2 + 4 files changed, 45 insertions(+) create mode 100644 apps/frontend/data/tasks/07-battle-for-azeroth/index.ts create mode 100644 apps/frontend/data/tasks/07-battle-for-azeroth/nazjatar.ts diff --git a/apps/frontend/data/tasks/07-battle-for-azeroth/index.ts b/apps/frontend/data/tasks/07-battle-for-azeroth/index.ts new file mode 100644 index 000000000..f355a58af --- /dev/null +++ b/apps/frontend/data/tasks/07-battle-for-azeroth/index.ts @@ -0,0 +1,3 @@ +import { bfaNazjatar } from './nazjatar'; + +export const bfaTasks = [bfaNazjatar]; diff --git a/apps/frontend/data/tasks/07-battle-for-azeroth/nazjatar.ts b/apps/frontend/data/tasks/07-battle-for-azeroth/nazjatar.ts new file mode 100644 index 000000000..53a33ccb6 --- /dev/null +++ b/apps/frontend/data/tasks/07-battle-for-azeroth/nazjatar.ts @@ -0,0 +1,38 @@ +import { iconLibrary } from '@/shared/icons'; +import { DbResetType } from '@/shared/stores/db/enums'; +import type { Task } from '@/types/tasks'; + +export const bfaNazjatar: Task = { + key: 'bfaNazjatar', + name: '[BfA] Nazjatar', + shortName: 'Naz', + showSeparate: true, + chores: [ + { + key: 'ancientBark', + name: '{item:170184}', + icon: iconLibrary.gameTreeFace, + alwaysStarted: true, + questReset: DbResetType.Weekly, + questIds: [57140], + }, + { + key: 'mardivas', + name: 'Laboritory of Mardivas', + icon: iconLibrary.gameFizzingFlask, + questReset: DbResetType.Weekly, + questIds: [55121], + }, + { + key: 'chooseFriend', + name: 'Choose Friend', + icon: iconLibrary.gameGoblinHead, + questReset: DbResetType.Daily, + questIds: [ + 57040, // Hunter Akana / Vim Brineheart + 57041, // Farseer Ori / Neri Sharpfin + 57042, // Bladesman Inowari / Poen Gillbrack + ], + }, + ], +}; diff --git a/apps/frontend/data/tasks/task-list.ts b/apps/frontend/data/tasks/task-list.ts index ca8dbdfa0..b608353dc 100644 --- a/apps/frontend/data/tasks/task-list.ts +++ b/apps/frontend/data/tasks/task-list.ts @@ -1,3 +1,4 @@ +import { bfaTasks } from './07-battle-for-azeroth'; import { slTasks } from './08-shadowlands'; import { dfTasks } from './09-dragonflight'; import { twwTasks } from './10-the-war-within'; @@ -9,6 +10,7 @@ import type { Task } from '@/types/tasks'; export const taskList: Task[] = [ ...eventTasks, ...pvpTasks, + ...bfaTasks, ...slTasks, ...dfTasks, ...twwTasks, diff --git a/apps/frontend/shared/icons/library.ts b/apps/frontend/shared/icons/library.ts index 0d6c0a9df..c87ea1601 100644 --- a/apps/frontend/shared/icons/library.ts +++ b/apps/frontend/shared/icons/library.ts @@ -48,6 +48,7 @@ export { default as gameFizzingFlask } from '~icons/game-icons/fizzing-flask'; export { default as gameFrog } from '~icons/game-icons/frog'; export { default as gameGauntlet } from '~icons/game-icons/gauntlet'; export { default as gameGladius } from '~icons/game-icons/gladius'; +export { default as gameGoblinHead } from '~icons/game-icons/goblin-head'; export { default as gameGreaves } from '~icons/game-icons/greaves'; export { default as gameHandBag } from '~icons/game-icons/hand-bag'; export { default as gameHanger } from '~icons/game-icons/hanger'; @@ -84,6 +85,7 @@ export { default as gameThorHammer } from '~icons/game-icons/thor-hammer'; export { default as gameThumbUp } from '~icons/game-icons/thumb-up'; export { default as gameTotemHead } from '~icons/game-icons/totem-head'; export { default as gameTreasureMap } from '~icons/game-icons/treasure-map'; +export { default as gameTreeFace } from '~icons/game-icons/tree-face'; export { default as gameTrident } from '~icons/game-icons/trident'; export { default as gameTrophy } from '~icons/game-icons/trophy'; export { default as gameTwirlyFlower } from '~icons/game-icons/twirly-flower'; From 1714b7be6242131d64c8475025081b3f3e3c881a Mon Sep 17 00:00:00 2001 From: Freddie Date: Fri, 17 Apr 2026 16:45:35 -0700 Subject: [PATCH 08/24] Fix split-icon icon alignment --- apps/frontend/scss/layout/split-icon.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/scss/layout/split-icon.scss b/apps/frontend/scss/layout/split-icon.scss index f1f1e4424..994798394 100644 --- a/apps/frontend/scss/layout/split-icon.scss +++ b/apps/frontend/scss/layout/split-icon.scss @@ -16,11 +16,11 @@ } img:nth-child(2) { - border: 2px solid var(--horde-border); + border: 2px solid var(--color-horde-border); clip-path: polygon(100% 100%, 0 100%, 100% 0); position: absolute; - bottom: 4px; - right: 2px; + bottom: 0; + right: 0; //margin-top: -64px; } } From 4ee2154f50fab88be80d32db58dd98e4fecf5297 Mon Sep 17 00:00:00 2001 From: Freddie Date: Fri, 17 Apr 2026 16:45:53 -0700 Subject: [PATCH 09/24] Debug spam~ --- .../characters/paperdoll/CharactersPaperdollEquipped.svelte | 5 ----- apps/frontend/utils/get-enchantment-text.ts | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/frontend/components/characters/paperdoll/CharactersPaperdollEquipped.svelte b/apps/frontend/components/characters/paperdoll/CharactersPaperdollEquipped.svelte index d54d18d08..7a6de2314 100644 --- a/apps/frontend/components/characters/paperdoll/CharactersPaperdollEquipped.svelte +++ b/apps/frontend/components/characters/paperdoll/CharactersPaperdollEquipped.svelte @@ -62,13 +62,8 @@ if (wowthingData.items.itemBonusCurrentSeason.has(bonusId)) { const upgrades = wowthingData.items.itemBonusToUpgrade[bonusId]; if (upgrades) { - console.log('uppies', upgrades); return upgrades; - } else { - console.log('no uppies'); } - } else { - console.log('not current?', bonusId); } } }; diff --git a/apps/frontend/utils/get-enchantment-text.ts b/apps/frontend/utils/get-enchantment-text.ts index a96ac3a25..28cfb68e1 100644 --- a/apps/frontend/utils/get-enchantment-text.ts +++ b/apps/frontend/utils/get-enchantment-text.ts @@ -14,5 +14,7 @@ export function getEnchantmentText(id: number, enchant: StaticDataEnchantment): } } + console.log(text, values); + return text; } From 94d8b43d9e4a8ca08d620783f4874496c2556eb6 Mon Sep 17 00:00:00 2001 From: Freddie Date: Fri, 17 Apr 2026 16:46:03 -0700 Subject: [PATCH 10/24] Add War Paints to recipe ignore list --- apps/tool/Data/IgnoredSkillLineAbilities.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/tool/Data/IgnoredSkillLineAbilities.cs b/apps/tool/Data/IgnoredSkillLineAbilities.cs index d99218176..f1288b923 100644 --- a/apps/tool/Data/IgnoredSkillLineAbilities.cs +++ b/apps/tool/Data/IgnoredSkillLineAbilities.cs @@ -217,6 +217,7 @@ public static partial class Hardcoded 127475, // MoP Incarnadine Ink 130407, // MoP Mystery of the Mists 176513, // WoD Draenor Merchant Order + 178240, // WoD War Paints 178550, // WoD Borrow Draenic Mortar 343687, // Shadowlands ?? 422337, // Lydaria's Binding Rune (temporary legendary craft) From 0e3f0c236eefc6778c3049b01fe75b14b58d93cc Mon Sep 17 00:00:00 2001 From: Freddie Date: Fri, 17 Apr 2026 16:46:19 -0700 Subject: [PATCH 11/24] Show moxie as green when you have 600+ --- apps/frontend/data/currencies.ts | 13 ++++++++++ .../components/currencies/Currency.svelte | 25 ++++++++++++------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/apps/frontend/data/currencies.ts b/apps/frontend/data/currencies.ts index 9abb3fd94..efeeb6051 100644 --- a/apps/frontend/data/currencies.ts +++ b/apps/frontend/data/currencies.ts @@ -795,4 +795,17 @@ export const pvpCurrencies = new Set([ export const currencyGood: Record = { [Constants.currencies.honor]: 2000, + + // Midnight - Crafting Moxie + 3256: 600, // Alchemy + 3257: 600, // Blacksmithing + 3258: 600, // Enchanting + 3259: 600, // Engineering + 3260: 600, // Herbalism + 3261: 600, // Inscription + 3262: 600, // Jewelcrafting + 3263: 600, // Leatherworking + 3264: 600, // Mining + 3265: 600, // Skinning + 3266: 600, // Tailoring }; diff --git a/apps/frontend/shared/components/currencies/Currency.svelte b/apps/frontend/shared/components/currencies/Currency.svelte index b0b6c2269..9658bb01b 100644 --- a/apps/frontend/shared/components/currencies/Currency.svelte +++ b/apps/frontend/shared/components/currencies/Currency.svelte @@ -1,5 +1,5 @@ @@ -47,7 +54,7 @@ {#if data} {@const { amount, percent, tooltip } = data} - {@const status = useStatusClass ? statusClass(percent) : ''} + {@const status = statusClass(percent)} {@const icon = useIconOverride ? currencyIconOverride[currency.id] : ''}
From 42a1c877ceaa6e27362d6bfd302e830249eeb314 Mon Sep 17 00:00:00 2001 From: Freddie Date: Fri, 17 Apr 2026 16:46:27 -0700 Subject: [PATCH 12/24] Random svelte stuff --- .../row/HomeTableRowProfessionsV2.svelte | 2 +- .../collections/recipes/View.svelte | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/frontend/components/home/table/row/HomeTableRowProfessionsV2.svelte b/apps/frontend/components/home/table/row/HomeTableRowProfessionsV2.svelte index 1558a78dc..d46689e08 100644 --- a/apps/frontend/components/home/table/row/HomeTableRowProfessionsV2.svelte +++ b/apps/frontend/components/home/table/row/HomeTableRowProfessionsV2.svelte @@ -45,7 +45,7 @@ -
- {#each taskChoices as choice (choice.id)} - - - - {/each} +{#snippet choiceCheckbox(choice: SettingsChoice)} + + + +{/snippet} + +
+

+ {task.name} + +

+ {#if taskFilter} +
+ {#each taskChoices as choice (choice.id)} + {@const choiceKey = `${task.key}_${choice.id}`} +
+ {@render choiceCheckbox(choice)} + {#if taskActive.includes(choice.id)} + + {/if} +
+ {/each} +
+ {:else} +
+ {#each taskChoices as choice (choice.id)} + {@render choiceCheckbox(choice)} + {/each} +
+ {/if}
diff --git a/apps/frontend/user-home/components/settings/sections/views/Tasks.svelte b/apps/frontend/user-home/components/settings/sections/views/Tasks.svelte index 54e685372..2ef5bcdf4 100644 --- a/apps/frontend/user-home/components/settings/sections/views/Tasks.svelte +++ b/apps/frontend/user-home/components/settings/sections/views/Tasks.svelte @@ -64,12 +64,7 @@ {#each taskList as task (task.key)} {#if task.chores.length > 1 && view.homeTasks.indexOf(task.key) >= 0} -
-
-

{task.name}

- -
-
+ {/if} {/each} {/if} diff --git a/apps/frontend/user-home/components/settings/sections/views/Views.svelte b/apps/frontend/user-home/components/settings/sections/views/Views.svelte index 91c82b737..cef47da36 100644 --- a/apps/frontend/user-home/components/settings/sections/views/Views.svelte +++ b/apps/frontend/user-home/components/settings/sections/views/Views.svelte @@ -25,6 +25,7 @@ homeProfessionsV2: [], homeProgress: [], homeTasks: [], + choreFilters: {}, disabledChores: {}, }; diff --git a/apps/frontend/user-home/state/user/derived.svelte.ts b/apps/frontend/user-home/state/user/derived.svelte.ts index 4631b29cd..39316d2e8 100644 --- a/apps/frontend/user-home/state/user/derived.svelte.ts +++ b/apps/frontend/user-home/state/user/derived.svelte.ts @@ -18,6 +18,7 @@ import { ManualDataSetGroup, type ManualDataSetGroupArray, } from '@/types/data/manual'; +import { useCharacterFilter } from '@/utils/characters'; import { getNextDailyResetFromTime, getNextWeeklyResetFromTime } from '@/utils/get-next-reset'; import type { UserQuestDataCharacterProgressObjective } from '@/types/data'; import type { Chore, Task } from '@/types/tasks'; @@ -497,6 +498,22 @@ export class DataUserDerived { return null; } + // Any chore with filters is scary + const filter = + settingsState.activeView.choreFilters?.[`${task.key}_${chore.key}`] || + settingsState.activeView.choreFilters?.[task.key]; + if (filter && filter !== 'any') { + const validForFilter = useCharacterFilter( + settingsState.value, + () => true, + character, + filter + ); + if (!validForFilter) { + return null; + } + } + const charChore = new CharacterChore(chore.key, undefined); if (chore.questCount) { charChore.progressTotal = chore.questCount; diff --git a/packages/csharp-lib/Models/ApplicationUserSettings.cs b/packages/csharp-lib/Models/ApplicationUserSettings.cs index 53ffd2bbe..550e48491 100644 --- a/packages/csharp-lib/Models/ApplicationUserSettings.cs +++ b/packages/csharp-lib/Models/ApplicationUserSettings.cs @@ -449,6 +449,7 @@ public class ApplicationUserSettingsView public List HomeProgress { get; set; } = new(); public List HomeTasks { get; set; } = new(); + public Dictionary ChoreFilters { get; set; } = new(); public Dictionary> DisabledChores { get; set; } = new(); } #nullable restore From 273e2f1f85c15cc94ebfbab38add60f515ac02db Mon Sep 17 00:00:00 2001 From: Freddie Date: Sat, 18 Apr 2026 16:39:47 -0700 Subject: [PATCH 18/24] Show item bind state in item search results --- .../items/ItemsSearchItemRow.svelte | 81 ++++++++++++------- .../items/ItemsSearchItemTable.svelte | 2 +- apps/frontend/enums/item-binding.ts | 13 +++ apps/frontend/shared/icons/library.ts | 3 + apps/frontend/types/items/search.ts | 3 + .../Search/ItemSearchResponseCharacter.cs | 4 + .../Search/ItemSearchResponseWarbank.cs | 4 + 7 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 apps/frontend/enums/item-binding.ts diff --git a/apps/frontend/components/items/ItemsSearchItemRow.svelte b/apps/frontend/components/items/ItemsSearchItemRow.svelte index f5050669e..73c80d0de 100644 --- a/apps/frontend/components/items/ItemsSearchItemRow.svelte +++ b/apps/frontend/components/items/ItemsSearchItemRow.svelte @@ -1,6 +1,8 @@ +{#snippet bindType(bindType: ItemBinding, bound: boolean)} + {#if bound} + + {:else if [ItemBinding.BindToBnetAccount, ItemBinding.BindToAccountUntilEquipped].includes(bindType)} + + {/if} +{/snippet} + {#if warbankItem} + {@render bindType(warbankItem.bindType, warbankItem.bound)} + {@render bindType(characterItem.bindType, characterItem.bound)} {character.name} @@ -76,12 +105,8 @@ {/if} - - {#if realm} - {Region[realm.region]}-{realm.name} - {:else} - {Region[region]} - {/if} + + {realmName} {#if characterItem} diff --git a/apps/frontend/components/items/ItemsSearchItemTable.svelte b/apps/frontend/components/items/ItemsSearchItemTable.svelte index 512082fd8..2dc4b50cf 100644 --- a/apps/frontend/components/items/ItemsSearchItemTable.svelte +++ b/apps/frontend/components/items/ItemsSearchItemTable.svelte @@ -8,7 +8,7 @@ import Row from './ItemsSearchItemRow.svelte'; import WowthingImage from '@/shared/components/images/sources/WowthingImage.svelte'; - export let response: ItemSearchResponseItem[]; + let { response }: { response: ItemSearchResponseItem[] } = $props(); type Sigh = { characterItems: ItemSearchResponseCharacter[]; diff --git a/apps/frontend/enums/item-binding.ts b/apps/frontend/enums/item-binding.ts new file mode 100644 index 000000000..7d17e3eef --- /dev/null +++ b/apps/frontend/enums/item-binding.ts @@ -0,0 +1,13 @@ +// Enum.TooltipDataItemBinding +export enum ItemBinding { + Account = 1, // Warbound + BnetAccount = 2, // ?? + Soulbound = 3, + BindToAccount = 4, // ?? + BindToBnetAccount = 5, // Binds to Warband + BindOnPickup = 6, + BindOnEquip = 7, + BindOnUse = 8, + AccountUntilEquipped = 9, // Warbound until equipped + BindToAccountUntilEquipped = 10, // Binds to Warband until equipped +} diff --git a/apps/frontend/shared/icons/library.ts b/apps/frontend/shared/icons/library.ts index c87ea1601..a23d1511f 100644 --- a/apps/frontend/shared/icons/library.ts +++ b/apps/frontend/shared/icons/library.ts @@ -60,10 +60,13 @@ export { default as gameJigsawBox } from '~icons/game-icons/jigsaw-box'; export { default as gameKnapsack } from '~icons/game-icons/knapsack'; export { default as gameLabradorHead } from '~icons/game-icons/labrador-head'; export { default as gameLockedFortress } from '~icons/game-icons/locked-fortress'; +export { default as gameLockedHeart } from '~icons/game-icons/locked-heart'; export { default as gameMetalBoot } from '~icons/game-icons/metal-boot'; export { default as gameNightSky } from '~icons/game-icons/night-sky'; export { default as gameNotebook } from '~icons/game-icons/notebook'; export { default as gameOpenChest } from '~icons/game-icons/open-chest'; +export { default as gamePadlock } from '~icons/game-icons/padlock'; +export { default as gamePadlockOpen } from '~icons/game-icons/padlock-open'; export { default as gamePiranha } from '~icons/game-icons/piranha'; export { default as gamePocketBow } from '~icons/game-icons/pocket-bow'; export { default as gamePokecog } from '~icons/game-icons/pokecog'; diff --git a/apps/frontend/types/items/search.ts b/apps/frontend/types/items/search.ts index 4d2df75af..7edd40784 100644 --- a/apps/frontend/types/items/search.ts +++ b/apps/frontend/types/items/search.ts @@ -1,3 +1,4 @@ +import type { BindType } from '@/enums/bind-type'; import type { ItemLocation } from '@/enums/item-location'; import type { ItemQuality } from '@/enums/item-quality'; import type { Region } from '@/enums/region'; @@ -12,6 +13,8 @@ export interface ItemSearchResponseItem { } export interface ItemSearchResponseCommon { + bindType: BindType; + bound: boolean; count: number; itemLevel: number; quality: ItemQuality; diff --git a/apps/web/Models/Search/ItemSearchResponseCharacter.cs b/apps/web/Models/Search/ItemSearchResponseCharacter.cs index 1342a835c..f0d9a884f 100644 --- a/apps/web/Models/Search/ItemSearchResponseCharacter.cs +++ b/apps/web/Models/Search/ItemSearchResponseCharacter.cs @@ -9,11 +9,13 @@ public class ItemSearchResponseCharacter public int CharacterId { get; set; } public int Count { get; set; } + public short BindType { get; set; } public short Context { get; set; } public short EnchantId { get; set; } public short ItemLevel { get; set; } public short Quality { get; set; } public short SuffixId { get; set; } + public bool Bound { get; set; } public List BonusIds { get; set; } public List Gems { get; set; } @@ -23,6 +25,8 @@ public ItemSearchResponseCharacter() public ItemSearchResponseCharacter(BasePlayerItem item) { + BindType = item.BindType; + Bound = item.Bound; Count = item.Count; Context = item.Context; EnchantId = item.EnchantId; diff --git a/apps/web/Models/Search/ItemSearchResponseWarbank.cs b/apps/web/Models/Search/ItemSearchResponseWarbank.cs index 758870576..8307493a9 100644 --- a/apps/web/Models/Search/ItemSearchResponseWarbank.cs +++ b/apps/web/Models/Search/ItemSearchResponseWarbank.cs @@ -10,11 +10,13 @@ public class ItemSearchResponseWarbank public short Tab { get; set; } public short Slot { get; set; } public int Count { get; set; } + public short BindType { get; set; } public short Context { get; set; } public short EnchantId { get; set; } public short ItemLevel { get; set; } public short Quality { get; set; } public short SuffixId { get; set; } + public bool Bound { get; set; } public List BonusIds { get; set; } public List Gems { get; set; } @@ -24,6 +26,8 @@ public ItemSearchResponseWarbank(PlayerWarbankItem item) Tab = item.ContainerId; Slot = item.Slot; + BindType = item.BindType; + Bound = item.Bound; Count = item.Count; Context = item.Context; EnchantId = item.EnchantId; From 195b3ff037f92356b311f8b2f2dd86e111c2ab78 Mon Sep 17 00:00:00 2001 From: Freddie Date: Sat, 18 Apr 2026 16:41:51 -0700 Subject: [PATCH 19/24] Fix RadioGroup negative margin --- apps/frontend/shared/components/forms/RadioGroup.svelte | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/frontend/shared/components/forms/RadioGroup.svelte b/apps/frontend/shared/components/forms/RadioGroup.svelte index 99e0e8f46..69f2fb443 100644 --- a/apps/frontend/shared/components/forms/RadioGroup.svelte +++ b/apps/frontend/shared/components/forms/RadioGroup.svelte @@ -23,10 +23,6 @@ :global(span) { margin-left: 0; - - :global(svg) { - margin-right: -0.75rem; - } } } From 33a9fb089e6029bf8dc3b8ca02f02dd754e73949 Mon Sep 17 00:00:00 2001 From: Freddie Date: Sat, 18 Apr 2026 16:48:42 -0700 Subject: [PATCH 20/24] Fix (mostly) item search layout mess --- .../items/ItemsSearchCharacterRow.svelte | 17 ++++++++--------- .../items/ItemsSearchCharacterTable.svelte | 10 +++++++++- apps/frontend/scss/layout/search-table.scss | 8 +++++--- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/frontend/components/items/ItemsSearchCharacterRow.svelte b/apps/frontend/components/items/ItemsSearchCharacterRow.svelte index 2cbc9c71c..0e3c3eb96 100644 --- a/apps/frontend/components/items/ItemsSearchCharacterRow.svelte +++ b/apps/frontend/components/items/ItemsSearchCharacterRow.svelte @@ -13,16 +13,15 @@ import WowthingImage from '@/shared/components/images/sources/WowthingImage.svelte'; - export let characterItem: ItemSearchResponseCharacter = null; - export let guildBankItem: ItemSearchResponseGuildBank = null; - export let itemId: number; + type Props = { + itemId: number; + characterItem?: ItemSearchResponseCharacter; + guildBankItem?: ItemSearchResponseGuildBank; + }; + let { itemId, characterItem, guildBankItem }: Props = $props(); - let item: ItemSearchResponseCommon; - let show: boolean; - $: { - item = characterItem || guildBankItem; - show = $itemSearchState.minimumQuality <= item.quality; - } + let item: ItemSearchResponseCommon = $derived(characterItem || guildBankItem); + let show = $derived($itemSearchState.minimumQuality <= item.quality); {#if show} diff --git a/apps/frontend/components/items/ItemsSearchCharacterTable.svelte b/apps/frontend/components/items/ItemsSearchCharacterTable.svelte index 3ccf6d8b8..8f1dc7b2f 100644 --- a/apps/frontend/components/items/ItemsSearchCharacterTable.svelte +++ b/apps/frontend/components/items/ItemsSearchCharacterTable.svelte @@ -55,6 +55,14 @@ } + + {#each characters as [character, items] (character.id)} @@ -74,7 +82,7 @@ {#each items as characterItem (characterItem)} {/each} -