From cacc029afd489d9b48a56b626a1c93c9199f6512 Mon Sep 17 00:00:00 2001 From: Freddie Date: Sun, 14 Jun 2026 18:58:36 -0700 Subject: [PATCH 1/8] Refactor world quests API into dynamic data (world quests + delves + ???) --- .../row/HomeTableRowGoldWorldQuests.svelte | 10 +- .../components/zone-maps/ZoneMapsThing.svelte | 4 +- apps/frontend/data/tasks/11-midnight/12-0.ts | 6 +- .../types.ts => types/world-quests.ts} | 14 -- apps/frontend/user-home/Main.svelte | 6 +- .../world-quests/ContinentBox.svelte | 2 +- .../components/world-quests/Map.svelte | 13 +- .../components/world-quests/Tooltip.svelte | 2 +- .../components/world-quests/WorldQuest.svelte | 2 +- .../user-home/components/world-quests/data.ts | 2 +- .../components/world-quests/store.ts | 107 -------------- apps/frontend/user-home/stores/dynamicData.ts | 134 ++++++++++++++++++ ...Controller.cs => DynamicDataController.cs} | 20 ++- apps/web/Models/Api/ApiDynamicData.cs | 11 ++ apps/web/Services/MemoryCacheService.cs | 33 ++++- .../csharp-lib/Constants/MemoryCacheKeys.cs | 1 + .../WorldQuestAggregateConverter.cs | 19 +++ .../csharp-lib/Models/WorldQuestAggregate.cs | 5 +- 18 files changed, 230 insertions(+), 161 deletions(-) rename apps/frontend/{user-home/components/world-quests/types.ts => types/world-quests.ts} (66%) delete mode 100644 apps/frontend/user-home/components/world-quests/store.ts create mode 100644 apps/frontend/user-home/stores/dynamicData.ts rename apps/web/Controllers/API/{WorldQuestsController.cs => DynamicDataController.cs} (57%) create mode 100644 apps/web/Models/Api/ApiDynamicData.cs create mode 100644 packages/csharp-lib/Converters/WorldQuestAggregateConverter.cs diff --git a/apps/frontend/components/home/table/row/HomeTableRowGoldWorldQuests.svelte b/apps/frontend/components/home/table/row/HomeTableRowGoldWorldQuests.svelte index 4248454e1..a1dbb24bb 100644 --- a/apps/frontend/components/home/table/row/HomeTableRowGoldWorldQuests.svelte +++ b/apps/frontend/components/home/table/row/HomeTableRowGoldWorldQuests.svelte @@ -5,11 +5,11 @@ import { timeState } from '@/shared/state/time.svelte'; import { componentTooltip } from '@/shared/utils/tooltips'; import { zoneMap } from '@/user-home/components/world-quests/data'; - import { worldQuestStore } from '@/user-home/components/world-quests/store'; + import { dynamicDataStore } from '@/user-home/stores/dynamicData'; import { userState } from '@/user-home/state/user'; import { getNumberKeyedEntries } from '@/utils/get-number-keyed-entries'; import type { CharacterProps } from '@/types/props'; - import type { ApiWorldQuest } from '@/user-home/components/world-quests/types'; + import type { ApiWorldQuest } from '@/types/world-quests'; import Tooltip from '@/components/tooltips/GoldWorldQuests.svelte'; @@ -93,10 +93,10 @@ {#if goldWorldQuests?.length > 0} - {#await worldQuestStore.fetch(character.realm.region)} + {#await dynamicDataStore.fetch(character.realm.region)} ... - {:then worldQuests} - {@const [count, active] = processQuests(worldQuests, questMap)} + {:then dynamicData} + {@const [count, active] = processQuests(dynamicData.worldQuests, questMap)}
= Object.fromEntrie const specialAssignmentFunc = (index: number, isQuest: boolean) => { return (char: Character, chore: Chore) => { const now = timeState.slowTime; - const allWorldQuests = worldQuestStore.getCachedQuests(char.region); + const allWorldQuests = dynamicDataStore.getCachedQuests(char.region); const questIdIndexes = Object.fromEntries( allWorldQuests .filter((worldQuest) => worldQuest.expires > now) @@ -60,7 +60,7 @@ const specialAssignmentFunc = (index: number, isQuest: boolean) => { }; const specialAssignmentExpiry: Chore['customExpiryFunc'] = (char, scannedAt, questIds) => { - const allWorldQuests = worldQuestStore.getCachedQuests(char.region); + const allWorldQuests = dynamicDataStore.getCachedQuests(char.region); const findQuestId = specialAssignmentQuestToUnlock[questIds[0]] || questIds[0]; const worldQuest = allWorldQuests.find((wq) => wq.questId === findQuestId); return worldQuest?.expires || Constants.defaultTime; diff --git a/apps/frontend/user-home/components/world-quests/types.ts b/apps/frontend/types/world-quests.ts similarity index 66% rename from apps/frontend/user-home/components/world-quests/types.ts rename to apps/frontend/types/world-quests.ts index a55c8c81c..db5225205 100644 --- a/apps/frontend/user-home/components/world-quests/types.ts +++ b/apps/frontend/types/world-quests.ts @@ -1,20 +1,6 @@ import type { RewardType } from '@/enums/reward-type'; import type { DateTime } from 'luxon'; -export interface ApiWorldQuestRaw { - jsonData: string; - questId: number; - region: number; - zoneId: number; -} - -export interface ApiWorldQuestJson { - count: number; - expirations: Record; - locations: Record; - rewards: Record>; -} - export interface ApiWorldQuest { expires: DateTime; locationX: string; diff --git a/apps/frontend/user-home/Main.svelte b/apps/frontend/user-home/Main.svelte index e8d324aae..086a099c3 100644 --- a/apps/frontend/user-home/Main.svelte +++ b/apps/frontend/user-home/Main.svelte @@ -9,8 +9,8 @@ import { timeStore } from '@/shared/stores/time'; import { delegateBasicTooltips } from '@/shared/utils/tooltips'; import { userAchievementStore, userQuestStore, userStore } from '@/stores'; - import { worldQuestStore } from '@/user-home/components/world-quests/store'; import { userState } from '@/user-home/state/user'; + import { dynamicDataStore } from '@/user-home/stores/dynamicData'; import { hashObject } from '@/utils/hash-object.svelte'; import parseApiTime from '@/utils/parse-api-time'; import type { Settings } from '@/shared/stores/settings/types/settings'; @@ -41,8 +41,8 @@ userAchievementStore.fetch(), userQuestStore.fetch(), userStore.fetch(), - worldQuestStore.fetch(Region.US), - worldQuestStore.fetch(Region.EU), + dynamicDataStore.fetch(Region.US), + dynamicDataStore.fetch(Region.EU), ]); userStore.setup(settingsState.value, $userStore); diff --git a/apps/frontend/user-home/components/world-quests/ContinentBox.svelte b/apps/frontend/user-home/components/world-quests/ContinentBox.svelte index 99e620755..4628b3107 100644 --- a/apps/frontend/user-home/components/world-quests/ContinentBox.svelte +++ b/apps/frontend/user-home/components/world-quests/ContinentBox.svelte @@ -3,7 +3,7 @@ import { wowthingData } from '@/shared/stores/data'; import { leftPad } from '@/utils/formatting'; - import type { ApiWorldQuest, WorldQuestZone } from './types'; + import type { ApiWorldQuest, WorldQuestZone } from '@/types/world-quests'; import WorldQuest from './WorldQuest.svelte'; diff --git a/apps/frontend/user-home/components/world-quests/Map.svelte b/apps/frontend/user-home/components/world-quests/Map.svelte index 5fd900754..33935401d 100644 --- a/apps/frontend/user-home/components/world-quests/Map.svelte +++ b/apps/frontend/user-home/components/world-quests/Map.svelte @@ -2,9 +2,9 @@ import find from 'lodash/find'; import { zoneData } from './data'; - import { worldQuestStore } from './store'; import { browserState } from '@/shared/state/browser.svelte'; import { settingsState } from '@/shared/state/settings.svelte'; + import { dynamicDataStore } from '@/user-home/stores/dynamicData'; import ContinentBox from './ContinentBox.svelte'; import Image from '@/shared/components/images/Image.svelte'; @@ -44,13 +44,16 @@ height={1000} /> - {#await worldQuestStore.fetch(browserState.current.worldQuests.region)} + {#await dynamicDataStore.fetch(browserState.current.worldQuests.region)} L O A D I N G . . . - {:then worldQuests} + {:then dynamicData} {#each (zone.children || []).filter((zone) => zone?.continentPoint) as childZone (childZone.id)} - + {:else} - {#each worldQuests[zone.id] || [] as worldQuest (worldQuest.questId)} + {#each dynamicData.worldQuests[zone.id] || [] as worldQuest (worldQuest.questId)} {/each} {/each} diff --git a/apps/frontend/user-home/components/world-quests/Tooltip.svelte b/apps/frontend/user-home/components/world-quests/Tooltip.svelte index bf411997c..38ee7c4af 100644 --- a/apps/frontend/user-home/components/world-quests/Tooltip.svelte +++ b/apps/frontend/user-home/components/world-quests/Tooltip.svelte @@ -6,9 +6,9 @@ import { userState } from '@/user-home/state/user'; import { toNiceDuration } from '@/utils/formatting'; import type { Character } from '@/types/character'; + import type { ApiWorldQuest } from '@/types/world-quests'; import { worldQuestPrereqs } from './data'; - import type { ApiWorldQuest } from './types'; import ParsedText from '@/shared/components/parsed-text/ParsedText.svelte'; import FactionIcon from '@/shared/components/images/FactionIcon.svelte'; diff --git a/apps/frontend/user-home/components/world-quests/WorldQuest.svelte b/apps/frontend/user-home/components/world-quests/WorldQuest.svelte index 34be6833f..e60a2906a 100644 --- a/apps/frontend/user-home/components/world-quests/WorldQuest.svelte +++ b/apps/frontend/user-home/components/world-quests/WorldQuest.svelte @@ -7,9 +7,9 @@ import { QuestInfoFlags, QuestInfoType } from '@/shared/stores/static/enums'; import { componentTooltip } from '@/shared/utils/tooltips'; import { toNiceNumber } from '@/utils/formatting/to-nice-number'; + import type { ApiWorldQuest } from '@/types/world-quests'; import { questInfoIcon } from './data'; - import type { ApiWorldQuest } from './types'; import FactionIcon from '@/shared/components/images/FactionIcon.svelte'; import IconifyWrapper from '@/shared/components/images/IconifyWrapper.svelte'; diff --git a/apps/frontend/user-home/components/world-quests/data.ts b/apps/frontend/user-home/components/world-quests/data.ts index 777963cf8..635715fdd 100644 --- a/apps/frontend/user-home/components/world-quests/data.ts +++ b/apps/frontend/user-home/components/world-quests/data.ts @@ -1,5 +1,5 @@ import { QuestInfoType } from '@/shared/stores/static/enums'; -import type { WorldQuestZone } from './types'; +import type { WorldQuestZone } from '@/types/world-quests'; export const zoneData: WorldQuestZone[] = [ { diff --git a/apps/frontend/user-home/components/world-quests/store.ts b/apps/frontend/user-home/components/world-quests/store.ts deleted file mode 100644 index e2b82f548..000000000 --- a/apps/frontend/user-home/components/world-quests/store.ts +++ /dev/null @@ -1,107 +0,0 @@ -import sortBy from 'lodash/sortBy'; -import sum from 'lodash/sum'; -import { DateTime } from 'luxon'; - -import type { - ApiWorldQuestRaw, - ApiWorldQuestJson, - ApiWorldQuest, - ApiWorldQuestReward, -} from './types'; -import type { RewardType } from '@/enums/reward-type'; - -class WorldQuestStore { - private static url = '/api/world-quests/active'; - private cache: Record> = {}; - private questsCache: Record = {}; - - async fetch(region: number) { - if (!this.cache[region]) { - const xsrf = document.getElementById('app').getAttribute('data-xsrf'); - - const response = await fetch(`${WorldQuestStore.url}/${region}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - RequestVerificationToken: xsrf, - }, - }); - - if (response.ok) { - const responseData = (await response.json()) as ApiWorldQuestRaw[]; - - const finalData: (typeof this.cache)[number] = (this.cache[region] = {}); - for (const apiWorldQuest of responseData) { - const jsonData = JSON.parse(apiWorldQuest.jsonData) as ApiWorldQuestJson; - - const expires = DateTime.fromISO( - sortBy( - Object.entries(jsonData.expirations), - ([, value]) => 1_000_000 - value - )[0][0], - { zone: 'utc' } - ); - const locationParts = sortBy( - Object.entries(jsonData.locations), - ([, value]) => 1_000_000 - value - )[0][0].split(' '); - - const worldQuest: ApiWorldQuest = { - expires, - locationX: locationParts[0], - locationY: locationParts[1], - questId: apiWorldQuest.questId, - rewards: [], - }; - - for (const [rewardKey, rewardCounts] of Object.entries(jsonData.rewards)) { - const rewardReports = sum(Object.values(rewardCounts)); - const rewards: ApiWorldQuestReward[] = []; - - for (const rewardString of rewardKey.split('|')) { - const rewardParts = rewardString.split('-'); - rewards.push({ - type: parseInt(rewardParts[0]) as RewardType, - id: parseInt(rewardParts[1]), - amount: parseInt(rewardParts[2]), - }); - } - - worldQuest.rewards.push([rewardReports, rewards]); - } - - (finalData[apiWorldQuest.zoneId] ||= []).push(worldQuest); - } - } - } - - return this.cache[region] || {}; - } - - getCached(region: number): Record { - return this.cache[region] || {}; - } - - getCachedQuests(region: number) { - if (!this.questsCache[region]) { - const zones = this.cache[region] || {}; - const questIdToExpiry = new Map(); - for (const worldQuests of Object.values(zones)) { - for (const worldQuest of worldQuests) { - questIdToExpiry.set(worldQuest.questId, worldQuest); - } - } - const allQuests = sortBy( - Array.from(questIdToExpiry.values()), - (worldQuest) => worldQuest.expires - ); - this.questsCache[region] = allQuests; - } - return this.questsCache[region]; - } - - isLoaded(region: number): boolean { - return !!this.cache[region]; - } -} -export const worldQuestStore = new WorldQuestStore(); diff --git a/apps/frontend/user-home/stores/dynamicData.ts b/apps/frontend/user-home/stores/dynamicData.ts new file mode 100644 index 000000000..0e10b92d7 --- /dev/null +++ b/apps/frontend/user-home/stores/dynamicData.ts @@ -0,0 +1,134 @@ +import sortBy from 'lodash/sortBy'; +import sum from 'lodash/sum'; +import { DateTime } from 'luxon'; + +import { getNumberKeyedEntries } from '@/utils/get-number-keyed-entries'; +import type { ApiWorldQuest, ApiWorldQuestReward } from '@/types/world-quests'; +import type { RewardType } from '@/enums/reward-type'; + +interface ApiDynamicData { + region: number; + delves: [number, string][]; + worldQuests: Record; +} + +interface ApiWorldQuestJson { + count: number; + expirations: Record; + locations: Record; + rewards: Record>; +} + +class DynamicData { + public delves: { poiId: number; story: string }[] = []; + public worldQuests: Record = {}; +} + +class DynamicDataStore { + private static url = '/api/world-quests/active'; + private cache: Record = {}; + private questsCache: Record = {}; + + async fetch(region: number) { + if (!this.cache[region]) { + const xsrf = document.getElementById('app').getAttribute('data-xsrf'); + + const response = await fetch(`${DynamicDataStore.url}/${region}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + RequestVerificationToken: xsrf, + }, + }); + + if (response.ok) { + const responseData = (await response.json()) as ApiDynamicData; + + const finalData = (this.cache[region] = new DynamicData()); + + // Delves + for (const [poiId, story] of responseData.delves) { + finalData.delves.push({ poiId, story }); + } + + // World Quests + for (const [zoneId, apiWorldQuests] of getNumberKeyedEntries( + responseData.worldQuests + )) { + const zoneQuests: ApiWorldQuest[] = (finalData.worldQuests[zoneId] = []); + + for (const [questId, jsonData] of apiWorldQuests) { + const parsed = JSON.parse(jsonData) as ApiWorldQuestJson; + + const expires = DateTime.fromISO( + sortBy( + Object.entries(parsed.expirations), + ([, value]) => 1_000_000 - value + )[0][0], + { zone: 'utc' } + ); + const locationParts = sortBy( + Object.entries(parsed.locations), + ([, value]) => 1_000_000 - value + )[0][0].split(' '); + + const worldQuest: ApiWorldQuest = { + expires, + locationX: locationParts[0], + locationY: locationParts[1], + questId, + rewards: [], + }; + + for (const [rewardKey, rewardCounts] of Object.entries(parsed.rewards)) { + const rewardReports = sum(Object.values(rewardCounts)); + const rewards: ApiWorldQuestReward[] = []; + + for (const rewardString of rewardKey.split('|')) { + const rewardParts = rewardString.split('-'); + rewards.push({ + type: parseInt(rewardParts[0]) as RewardType, + id: parseInt(rewardParts[1]), + amount: parseInt(rewardParts[2]), + }); + } + + worldQuest.rewards.push([rewardReports, rewards]); + } + + zoneQuests.push(worldQuest); + } + } + } + } + + return this.getCached(region); + } + + getCached(region: number): DynamicData { + return (this.cache[region] ||= new DynamicData()); + } + + getCachedQuests(region: number) { + if (!this.questsCache[region]) { + const zones = this.getCached(region).worldQuests; + const questIdToExpiry = new Map(); + for (const worldQuests of Object.values(zones)) { + for (const worldQuest of worldQuests) { + questIdToExpiry.set(worldQuest.questId, worldQuest); + } + } + const allQuests = sortBy( + Array.from(questIdToExpiry.values()), + (worldQuest) => worldQuest.expires + ); + this.questsCache[region] = allQuests; + } + return this.questsCache[region]; + } + + isLoaded(region: number): boolean { + return !!this.cache[region]; + } +} +export const dynamicDataStore = new DynamicDataStore(); diff --git a/apps/web/Controllers/API/WorldQuestsController.cs b/apps/web/Controllers/API/DynamicDataController.cs similarity index 57% rename from apps/web/Controllers/API/WorldQuestsController.cs rename to apps/web/Controllers/API/DynamicDataController.cs index ffa1d46dd..b7c8d51af 100644 --- a/apps/web/Controllers/API/WorldQuestsController.cs +++ b/apps/web/Controllers/API/DynamicDataController.cs @@ -1,25 +1,28 @@ using System.Net.Mime; using Wowthing.Lib.Contexts; using Wowthing.Lib.Enums; +using Wowthing.Web.Services; namespace Wowthing.Web.Controllers.API; -[Route("api/world-quests")] -public class WorldQuestsController : Controller +public class DynamicDataController : Controller { private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly MemoryCacheService _memoryCacheService; private readonly WowDbContext _context; - public WorldQuestsController( + public DynamicDataController( JsonSerializerOptions jsonSerializerOptions, + MemoryCacheService memoryCacheService, WowDbContext context ) { _jsonSerializerOptions = jsonSerializerOptions; + _memoryCacheService = memoryCacheService; _context = context; } - [HttpGet("active/{region:int}")] + [HttpGet("api/dynamic-data/{region:int}")] public async Task Active([FromRoute] int region) { if (!Enum.IsDefined(typeof(WowRegion), region)) @@ -27,13 +30,8 @@ public async Task Active([FromRoute] int region) return BadRequest(); } - var results = await _context.WorldQuestAggregate - .Where(wqa => wqa.Region == (short)(region & 0x7FFF)) - .OrderBy(wqa => wqa.ZoneId) - .ThenBy(wqa => wqa.QuestId) - .ToArrayAsync(); - - string json = JsonSerializer.Serialize(results, _jsonSerializerOptions); + var result = await _memoryCacheService.GetDynamicDataForRegion((short)(region & 0x7FFF)); + string json = JsonSerializer.Serialize(result, _jsonSerializerOptions); return Content(json, MediaTypeNames.Application.Json); } } diff --git a/apps/web/Models/Api/ApiDynamicData.cs b/apps/web/Models/Api/ApiDynamicData.cs new file mode 100644 index 000000000..8970bad1b --- /dev/null +++ b/apps/web/Models/Api/ApiDynamicData.cs @@ -0,0 +1,11 @@ +using Wowthing.Lib.Enums; +using Wowthing.Lib.Models; + +namespace Wowthing.Web.Models.Api; + +public class ApiDynamicData +{ + public WowRegion Region { get; set; } + public object[] Delves { get; set; } + public Dictionary WorldQuests { get; set; } +} diff --git a/apps/web/Services/MemoryCacheService.cs b/apps/web/Services/MemoryCacheService.cs index 0dd38fc88..bc775dcc3 100644 --- a/apps/web/Services/MemoryCacheService.cs +++ b/apps/web/Services/MemoryCacheService.cs @@ -4,9 +4,11 @@ using StackExchange.Redis; using Wowthing.Lib.Constants; using Wowthing.Lib.Contexts; +using Wowthing.Lib.Enums; using Wowthing.Lib.Models; using Wowthing.Lib.Models.Wow; using Wowthing.Lib.Services; +using Wowthing.Web.Models.Api; namespace Wowthing.Web.Services; @@ -109,18 +111,37 @@ public void ExpireCommoditesForRegion(short region) _memoryCache.Remove(string.Format(MemoryCacheKeys.AuctionCommodities, region)); } - public async Task> GetBackgroundImages() + public async Task GetDynamicDataForRegion(short region) { return await _memoryCache.GetOrCreateAsync( - MemoryCacheKeys.BackgroundImages, - cacheEntry => + string.Format(MemoryCacheKeys.DynamicData, region), + async cacheEntry => { cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); - return _context.BackgroundImage + var worldQuestAggregates = await _context.WorldQuestAggregate .AsNoTracking() - .Where(bi => bi.Role == null) - .ToDictionaryAsync(bi => bi.Id); + .Where(wqa => wqa.Region == region) + .OrderBy(wqa => wqa.ZoneId) + .ThenBy(wqa => wqa.QuestId) + .ToArrayAsync(); + + var miscAggregates = await _context.MiscAggregate + .AsNoTracking() + .Where(ma => ma.Region == region) + .ToArrayAsync(); + + var delveAggregate = miscAggregates.FirstOrDefault(ma => ma.ReportType == MiscReportType.Delve); + object[] delveJson = delveAggregate != null + ? JsonSerializer.Deserialize(delveAggregate.JsonData) + : []; + + return new ApiDynamicData + { + Region = (WowRegion)region, + Delves = delveJson, + WorldQuests = worldQuestAggregates.ToGroupedDictionary(wqa => wqa.ZoneId), + }; } ); } diff --git a/packages/csharp-lib/Constants/MemoryCacheKeys.cs b/packages/csharp-lib/Constants/MemoryCacheKeys.cs index 6d10ca64b..545bba45b 100644 --- a/packages/csharp-lib/Constants/MemoryCacheKeys.cs +++ b/packages/csharp-lib/Constants/MemoryCacheKeys.cs @@ -7,6 +7,7 @@ public static class MemoryCacheKeys public const string AuctionHouseUpdatedTimes = "auction-house-updated-times"; public const string BackgroundImages = "background-images"; public const string DecorItems = "decor-items"; + public const string DynamicData = "dynamic-data:{0}"; public const string ItemBonuses = "item-bonuses"; public const string ItemClasses = "item-classes"; public const string ItemModifiedAppearances = "item-modified-appearances"; diff --git a/packages/csharp-lib/Converters/WorldQuestAggregateConverter.cs b/packages/csharp-lib/Converters/WorldQuestAggregateConverter.cs new file mode 100644 index 000000000..3d5e7a0b2 --- /dev/null +++ b/packages/csharp-lib/Converters/WorldQuestAggregateConverter.cs @@ -0,0 +1,19 @@ +using Wowthing.Lib.Models; + +namespace Wowthing.Lib.Converters; + +public class WorldQuestAggregateConverter : JsonConverter +{ + public override WorldQuestAggregate Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, WorldQuestAggregate aggregate, JsonSerializerOptions options) + { + writer.WriteStartArray(); + writer.WriteNumberValue(aggregate.QuestId); + writer.WriteStringValue(aggregate.JsonData); + writer.WriteEndArray(); + } +} diff --git a/packages/csharp-lib/Models/WorldQuestAggregate.cs b/packages/csharp-lib/Models/WorldQuestAggregate.cs index 17491d96a..6cf5243a2 100644 --- a/packages/csharp-lib/Models/WorldQuestAggregate.cs +++ b/packages/csharp-lib/Models/WorldQuestAggregate.cs @@ -1,5 +1,8 @@ -namespace Wowthing.Lib.Models; +using Wowthing.Lib.Converters; +namespace Wowthing.Lib.Models; + +[JsonConverter(typeof(WorldQuestAggregateConverter))] public class WorldQuestAggregate { // 4 bytes From 1f2eeff805af36319d54dff22f15ba24a60c1241 Mon Sep 17 00:00:00 2001 From: Freddie Date: Sun, 14 Jun 2026 19:02:47 -0700 Subject: [PATCH 2/8] Fix sorting home by custom task --- apps/frontend/utils/home/home-sort.ts | 53 ++++++++++++++------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/apps/frontend/utils/home/home-sort.ts b/apps/frontend/utils/home/home-sort.ts index 95c80a0f8..c7d345612 100644 --- a/apps/frontend/utils/home/home-sort.ts +++ b/apps/frontend/utils/home/home-sort.ts @@ -19,33 +19,33 @@ import type { Character } from '@/types'; const MAX_TIME = 2 ** 31 * 1000; -export function homeSort(char: Character, sortBy: string): string { - if (sortBy === 'gold') { +export function homeSort(char: Character, sortingBy: string): string { + if (sortingBy === 'gold') { return leftPad(10_000_000 - char.gold, 8, '0'); - } else if (sortBy === 'bagSpace') { + } else if (sortingBy === 'bagSpace') { return leftPad(1000 - char.bagSlotsFree, 4, '0'); - } else if (sortBy === 'bestItemLevel') { + } else if (sortingBy === 'bestItemLevel') { return leftPad( 10000 - Math.floor(parseFloat(char.bestItemLevels?.[char.activeSpecId]?.[0] || '0.0') * 10), 5, '0' ); - } else if (sortBy === 'itemLevel') { + } else if (sortingBy === 'itemLevel') { return leftPad( 10000 - Math.floor(parseFloat(char.calculatedItemLevel || '0.0') * 10), 5, '0' ); - } else if (sortBy === 'lastSeenAddon') { + } else if (sortingBy === 'lastSeenAddon') { return leftPad(MAX_TIME - (char.lastSeenAddon?.toMillis() || 0), 13, '0'); - } else if (sortBy === 'currentLocation') { + } else if (sortingBy === 'currentLocation') { // adding two spaces makes it sort before " > blah" return char.currentLocation + ' ' || 'ZZZZZ'; - } else if (sortBy === 'hearthLocation') { + } else if (sortingBy === 'hearthLocation') { // adding two spaces makes it sort before " > blah" return char.hearthLocation + ' ' || 'ZZZZZ'; - } else if (sortBy === 'keystone') { + } else if (sortingBy === 'keystone') { if (char.level === Constants.characterMaxLevel && char.weekly?.keystoneScannedAt) { const resetTime = getNextWeeklyReset(char.weekly.keystoneScannedAt, char.realm.region); if (resetTime > timeState.slowTime) { @@ -57,11 +57,11 @@ export function homeSort(char: Character, sortBy: string): string { } return '100|ZZ'; - } else if (sortBy === 'mythicPlusScore') { + } else if (sortingBy === 'mythicPlusScore') { const season = Constants.mythicPlusSeason; const rating = char.mythicPlusSeasonScores?.[season] || char.raiderIo?.[season]?.all || 0; return leftPad(Math.floor(100000 - rating * 10), 6, '0'); - } else if (sortBy === 'professionCooldowns') { + } else if (sortingBy === 'professionCooldowns') { const cooldownData = get(lazyStore).characters[char.id].professionCooldowns; return leftPad( 100 - (cooldownData.total > 0 ? (cooldownData.have / cooldownData.total) * 100 : -1), @@ -71,16 +71,16 @@ export function homeSort(char: Character, sortBy: string): string { // } else if (sortBy === 'professionWorkOrders') { // const orderData = lazyStore.characters[char.id].professionWorkOrders; // return leftPad(10 - (orderData.total > 0 ? orderData.have : -1), 3, '0'); - } else if (sortBy === 'restedExperience') { + } else if (sortingBy === 'restedExperience') { if (char.level === Constants.characterMaxLevel) { return '999'; } else { const [rested] = getCharacterRested(timeState.time, char); return leftPad(999 - parseInt(rested), 3, '0'); } - } else if (sortBy === 'statsSpeed') { + } else if (sortingBy === 'statsSpeed') { return leftPad(99999 - char.movementSpeed.total * 100, 5, '0'); - } else if (sortBy === 'vaultMythicPlus') { + } else if (sortingBy === 'vaultMythicPlus') { if (char.weekly?.vault?.generatedRewards) { return '000|000|000'; } else if (char.weekly?.vault?.availableRewards) { @@ -98,7 +98,7 @@ export function homeSort(char: Character, sortBy: string): string { leftPad(900 - levels[1], 3, '0'), leftPad(900 - levels[2], 3, '0'), ].join('|'); - } else if (sortBy === 'vaultRaid') { + } else if (sortingBy === 'vaultRaid') { if (char.weekly?.vault?.generatedRewards) { return '000|000|000'; } else if (char.weekly?.vault?.availableRewards) { @@ -111,7 +111,7 @@ export function homeSort(char: Character, sortBy: string): string { leftPad(900 - getRaidVaultItemLevel(progress?.[1])[0], 3, '0'), leftPad(900 - getRaidVaultItemLevel(progress?.[2])[0], 3, '0'), ].join('|'); - } else if (sortBy === 'vaultWorld') { + } else if (sortingBy === 'vaultWorld') { if (char.weekly?.vault?.generatedRewards) { return '000|000|000'; } else if (char.weekly?.vault?.availableRewards) { @@ -129,18 +129,18 @@ export function homeSort(char: Character, sortBy: string): string { leftPad(900 - levels[1], 3, '0'), leftPad(900 - levels[2], 3, '0'), ].join('|'); - } else if (sortBy.startsWith('currencies:')) { - const currencyId = parseInt(sortBy.split(':')[1]); + } else if (sortingBy.startsWith('currencies:')) { + const currencyId = parseInt(sortingBy.split(':')[1]); const value = currencyId > 1_000_000 ? char.getItemCount(currencyId - 1_000_000) : char.currencies?.[currencyId]?.quantity || 0; return leftPad(100_000_000 - value, 9, '0'); - } else if (sortBy.startsWith('items:')) { - const itemId = parseInt(sortBy.split(':')[1]); + } else if (sortingBy.startsWith('items:')) { + const itemId = parseInt(sortingBy.split(':')[1]); return leftPad(1000000 - char.getItemCount(itemId), 7, '0'); - } else if (sortBy.startsWith('lockouts:')) { - const lockoutKey = sortBy.split(':')[1]; + } else if (sortingBy.startsWith('lockouts:')) { + const lockoutKey = sortingBy.split(':')[1]; const lockout = char.lockouts?.[lockoutKey]; if (lockout?.locked === true) { return [ @@ -150,9 +150,9 @@ export function homeSort(char: Character, sortBy: string): string { } else { return '999|999'; } - } else if (sortBy.startsWith('progress:')) { + } else if (sortingBy.startsWith('progress:')) { let value = -1; - const progressParts = sortBy.split(':')[1].split('|'); + const progressParts = sortingBy.split(':')[1].split('|'); const category = wowthingData.manual.progressSets.find( (p) => p?.[0]?.slug === progressParts[0] ); @@ -170,8 +170,9 @@ export function homeSort(char: Character, sortBy: string): string { } } return leftPad(10_000 - value, 5, '0'); - } else if (sortBy.startsWith('tasks:')) { - const fullTaskName = sortBy.split(':')[1]; + } else if (sortingBy.startsWith('tasks:')) { + console.log(sortingBy); + const fullTaskName = sortingBy.split(':').splice(1).join(':'); const [, choreName] = fullTaskName.split('|', 2); const charTask = userState.activeViewTasks[char.id]?.[fullTaskName]; const charChore = charTask?.chores?.[choreName]; From 0ec646ef108fc2c37dc6978418ecb1d58b9930eb Mon Sep 17 00:00:00 2001 From: Freddie Date: Sun, 14 Jun 2026 19:05:03 -0700 Subject: [PATCH 3/8] Fix dynamic-data loading --- apps/frontend/user-home/stores/dynamicData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/user-home/stores/dynamicData.ts b/apps/frontend/user-home/stores/dynamicData.ts index 0e10b92d7..f3d580faf 100644 --- a/apps/frontend/user-home/stores/dynamicData.ts +++ b/apps/frontend/user-home/stores/dynamicData.ts @@ -25,7 +25,7 @@ class DynamicData { } class DynamicDataStore { - private static url = '/api/world-quests/active'; + private static url = '/api/dynamic-data'; private cache: Record = {}; private questsCache: Record = {}; From 2f30666d712c9bd094fcbacddd2cbe98971f74b5 Mon Sep 17 00:00:00 2001 From: Freddie Date: Sun, 14 Jun 2026 19:34:15 -0700 Subject: [PATCH 4/8] Add active delves to view switcher --- .../home/view-switcher/Delves.svelte | 46 +++++++++ .../home/view-switcher/DelvesTooltip.svelte | 37 +++++++ .../home/view-switcher/ViewSwitcher.svelte | 3 + apps/frontend/data/delve.ts | 98 +++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 apps/frontend/components/home/view-switcher/Delves.svelte create mode 100644 apps/frontend/components/home/view-switcher/DelvesTooltip.svelte create mode 100644 apps/frontend/data/delve.ts diff --git a/apps/frontend/components/home/view-switcher/Delves.svelte b/apps/frontend/components/home/view-switcher/Delves.svelte new file mode 100644 index 000000000..6ac3cd592 --- /dev/null +++ b/apps/frontend/components/home/view-switcher/Delves.svelte @@ -0,0 +1,46 @@ + + + + +
({ + delves, + }), + }} +> + + {#each delves as { poiId, story } (poiId)} + {@const delve = delveMap[poiId]} + {#if delve} +
+ {delve.shortName} +
+ {/if} + {/each} +
diff --git a/apps/frontend/components/home/view-switcher/DelvesTooltip.svelte b/apps/frontend/components/home/view-switcher/DelvesTooltip.svelte new file mode 100644 index 000000000..88263baff --- /dev/null +++ b/apps/frontend/components/home/view-switcher/DelvesTooltip.svelte @@ -0,0 +1,37 @@ + + + + +
+

Bountiful Delves

+ + + {#each delves as { poiId, story } (poiId)} + {@const delve = delveMap[poiId]} + {#if delve} + + + + + {/if} + {/each} + +
{delve.name}{story}
+
diff --git a/apps/frontend/components/home/view-switcher/ViewSwitcher.svelte b/apps/frontend/components/home/view-switcher/ViewSwitcher.svelte index 41f899ad9..e3052943e 100644 --- a/apps/frontend/components/home/view-switcher/ViewSwitcher.svelte +++ b/apps/frontend/components/home/view-switcher/ViewSwitcher.svelte @@ -7,6 +7,7 @@ import { sharedState } from '@/shared/state/shared.svelte'; import { userState } from '@/user-home/state/user'; + import Delves from './Delves.svelte'; import Holidays from './Holidays.svelte'; import IconifyWrapper from '@/shared/components/images/IconifyWrapper.svelte'; import ParagonQuests from '../table/paragon/ParagonQuests.svelte'; @@ -118,5 +119,7 @@ + +
diff --git a/apps/frontend/data/delve.ts b/apps/frontend/data/delve.ts new file mode 100644 index 000000000..227875bd0 --- /dev/null +++ b/apps/frontend/data/delve.ts @@ -0,0 +1,98 @@ +export type Delve = { + name: string; + shortName: string; + storyRanks: Record; +}; + +export const delveMap: Record = { + 8426: { + name: 'Collegiate Calamity', + shortName: 'CC', + storyRanks: { + 'Academy Under Siege': 4, + 'Faculty of Fear': 3, + 'Invasive Glow': 5, + }, + }, + 8428: { + name: 'Parhelion Plaza', + shortName: 'PP', + storyRanks: { + 'Bombing Run': 0, + 'Holding the Line': 4, + 'March of the Arcane Brigade': 0, + }, + }, + 8430: { + name: 'Sunkiller Sanctum', + shortName: 'SS', + storyRanks: { + 'Core of the Problem': 3, + 'Not What I Expected': 3, + 'The Gravitational Effect': 3, + }, + }, + 8432: { + name: 'Shadowguard Point', + shortName: 'SP', + storyRanks: { + Calamitous: 3, + 'Captured Wildlife': 0, + 'Stolen Mana': 0, + }, + }, + 8434: { + name: 'The Grudge Pit', + shortName: 'GP', + storyRanks: { + 'Arena Champion': 3, + 'Dastardly Rotstalk': 3, + 'Lightbloom Invasion': 3, + }, + }, + 8436: { + name: 'The Gulf of Memory', + shortName: 'GM', + storyRanks: { + 'Alnmoth Munchies': 3, + 'Descent of the Haranir': 3, + 'Sporasaur Special': 4, + }, + }, + 8438: { + name: 'The Shadow Enclave', + shortName: 'SE', + storyRanks: { + 'Mirror Shine': 0, + 'Shadowy Supplies': 3, + "Traitor's Due": 3, + }, + }, + 8440: { + name: 'The Darkway', + shortName: 'DW', + storyRanks: { + 'Focusers Under Pressure': 3, + 'Leyline Technician': 3, + 'Ogre Powered': 4, + }, + }, + 8442: { + name: 'Twilight Crypts', + shortName: 'TC', + storyRanks: { + 'Loosed Loa': 3, + 'Party Crasher': 3, + 'Trapped!': 3, + }, + }, + 8444: { + name: "Atal'Aman", + shortName: 'AA', + storyRanks: { + 'Ritual Interrupted': 3, + 'Toadly Unbecoming': 3, + 'Totem Annihilation': 3, + }, + }, +}; From b57bc204addf7bc7049c69e86eb650dfe472f545 Mon Sep 17 00:00:00 2001 From: Freddie Date: Sun, 14 Jun 2026 19:36:29 -0700 Subject: [PATCH 5/8] Fix hardcoded Region.US --- apps/frontend/components/home/view-switcher/Delves.svelte | 4 ++-- apps/frontend/components/zone-maps/ZoneMapsThing.svelte | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/frontend/components/home/view-switcher/Delves.svelte b/apps/frontend/components/home/view-switcher/Delves.svelte index 6ac3cd592..77b855c72 100644 --- a/apps/frontend/components/home/view-switcher/Delves.svelte +++ b/apps/frontend/components/home/view-switcher/Delves.svelte @@ -1,14 +1,14 @@