From 2d274303d784d9d3c0b421ab2380b7021a45755b Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 12:12:58 -0700 Subject: [PATCH 01/11] Fix backend HTTP connection limit --- apps/backend/Program.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/backend/Program.cs b/apps/backend/Program.cs index d4aeb88df..2536bc13a 100644 --- a/apps/backend/Program.cs +++ b/apps/backend/Program.cs @@ -30,8 +30,6 @@ public class Program public static void Main(string[] args) { - ServicePointManager.DefaultConnectionLimit = 10; - Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(Configuration) .Enrich.FromLogContext() @@ -135,6 +133,10 @@ private static void ConfigureServices(HostBuilderContext hostContext, IServiceCo { client.Timeout = TimeSpan.FromSeconds(120); }) + .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler + { + MaxConnectionsPerServer = 10, + }) .AddPolicyHandler(rateLimitPolicy) .SetHandlerLifetime(Timeout.InfiniteTimeSpan); From 6883ab0287d9c6fbe7fc91ac7cbdae2208d947e2 Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 12:13:17 -0700 Subject: [PATCH 02/11] Handle upload emissary quests data --- apps/backend/Jobs/User/UserUploadJob.cs | 129 ++++++++++++++------ apps/backend/Models/Uploads/Upload.cs | 1 + packages/csharp-lib/Enums/MiscReportType.cs | 4 + 3 files changed, 100 insertions(+), 34 deletions(-) diff --git a/apps/backend/Jobs/User/UserUploadJob.cs b/apps/backend/Jobs/User/UserUploadJob.cs index 20266d731..187dd29de 100644 --- a/apps/backend/Jobs/User/UserUploadJob.cs +++ b/apps/backend/Jobs/User/UserUploadJob.cs @@ -34,6 +34,13 @@ public class UserUploadJob : JobBase private static readonly Regex PlayerGuidRegex = new(@"^Player-\d+-([0-9A-Fa-f]+)$", RegexOptions.Compiled); private static readonly Regex ValidGuildRegex = new(@"^\d+/", RegexOptions.Compiled); + private static readonly Dictionary ExpansionToMiscReportType = new() + { + { 6, MiscReportType.LegionEmissaries }, + { 7, MiscReportType.BattleForAzerothEmissaries }, + { 8, MiscReportType.ShadowlandsCallings }, + }; + private static readonly AsyncRetryPolicy LockRetryPolicy = Policy .HandleResult(r => r == false) .WaitAndRetryAsync( @@ -1080,69 +1087,123 @@ await localContext.PlayerWarbankItem private async Task HandleMiscReports(WowRegion accountRegion, Upload parsedData) { - if (parsedData.Delves == null) - { - return; - } - await using var localContext = await ContextFactory.CreateDbContextAsync(); var reportMap = await localContext.MiscReport .Where(mr => mr.UserId == _userId && mr.Region == (short)accountRegion) .ToDictionaryAsync(mr => mr.ReportType); + var now = DateTime.UtcNow; long currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - DateTime? expiresAt = null; - List<(int, string)> delves = []; - foreach (string delveString in parsedData.Delves) + var reports = new List<(MiscReportType ReportType, DateTime ExpiresAt, string Data)>(); + + // Delves + if (parsedData.Delves != null) { - string[] parts = delveString.Split(":"); - if (parts.Length != 3) + DateTime? expiresAt = null; + List<(int, string)> delves = []; + foreach (string delveString in parsedData.Delves) { - continue; + string[] parts = delveString.Split(":"); + if (parts.Length != 3) + { + continue; + } + + // unixTimestamp:poiId:story + if (!int.TryParse(parts[0], out int validUntil) || + !int.TryParse(parts[1], out int poiId) || + string.IsNullOrWhiteSpace(parts[2]) || + validUntil < currentTimestamp || + poiId == 0) + { + // log + continue; + } + + expiresAt ??= validUntil.AsUtcDateTime(); + + delves.Add((poiId, parts[2])); } - // unixTimestamp:poiId:story - int.TryParse(parts[0], out int validUntil); - int.TryParse(parts[1], out int poiId); - if (validUntil < currentTimestamp || poiId == 0 || string.IsNullOrWhiteSpace(parts[2])) + if (expiresAt != null && delves.Count > 0) { - // log - continue; - } + List dataParts = []; + foreach ((int poiId, string story) in delves.OrderBy(tup => tup.Item1)) + { + dataParts.Add([poiId, story]); + } - expiresAt ??= validUntil.AsUtcDateTime(); + string data = JsonSerializer.Serialize(dataParts); - delves.Add((poiId, parts[2])); + reports.Add((MiscReportType.Delve, expiresAt.Value, data)); + } } - if (expiresAt != null && delves.Count > 0) + // Emissary Quests + if (parsedData.EmissaryQuests != null) { - List dataParts = []; - foreach ((int poiId, string story) in delves.OrderBy(tup => tup.Item1)) + foreach ((int expansion, var emissaryQuestStrings) in parsedData.EmissaryQuests) { - dataParts.Add([poiId, story]); - } + if (!ExpansionToMiscReportType.TryGetValue(expansion, out var reportType)) + { + // log + continue; + } + + List<(int QuestId, int ExpiresAt)> emissaryQuests = new(); + foreach (string emissaryQuestString in emissaryQuestStrings) + { + string[] parts = emissaryQuestString.Split(":"); + if (parts.Length != 2) + { + continue; + } - string data = JsonSerializer.Serialize(dataParts); + // questId:expires + if (!int.TryParse(parts[0], out int questId) || + !int.TryParse(parts[1], out int validUntil) || + questId == 0 || + validUntil < currentTimestamp) + { + // log + continue; + } - if (reportMap.TryGetValue(MiscReportType.Delve, out var dbReport)) - { - dbReport.Data = data; + emissaryQuests.Add((questId, validUntil)); + } + + if (emissaryQuests.Count > 0) + { + List dataParts = []; + foreach ((int questId, int expiresAt) in emissaryQuests.OrderBy(tup => tup.ExpiresAt)) + { + dataParts.Add([questId, expiresAt]); + } + + string data = JsonSerializer.Serialize(dataParts); + + reports.Add((reportType, emissaryQuests.Last().ExpiresAt.AsUtcDateTime(), data)); + } } - else + } + + foreach (var report in reports) + { + if (!reportMap.TryGetValue(report.ReportType, out var dbReport)) { dbReport = new MiscReport { UserId = _userId, - ExpiresAt = expiresAt.Value, - ReportedAt = DateTime.UtcNow, - ReportType = MiscReportType.Delve, + ReportedAt = now, + ReportType = report.ReportType, Region = (short)accountRegion, - Data = data, }; localContext.MiscReport.Add(dbReport); } + + dbReport.Data = report.Data; + dbReport.ExpiresAt = report.ExpiresAt; } await localContext.SaveChangesAsync(); diff --git a/apps/backend/Models/Uploads/Upload.cs b/apps/backend/Models/Uploads/Upload.cs index 35b68375d..5d482e522 100644 --- a/apps/backend/Models/Uploads/Upload.cs +++ b/apps/backend/Models/Uploads/Upload.cs @@ -21,6 +21,7 @@ public class Upload public Dictionary Decor { get; set; } public List Delves { get; set; } + public Dictionary> EmissaryQuests { get; set; } public List Illusions { get; set; } public Dictionary Paragons { get; set; } public List Quests { get; set; } diff --git a/packages/csharp-lib/Enums/MiscReportType.cs b/packages/csharp-lib/Enums/MiscReportType.cs index c50fb13a8..2a50839f7 100644 --- a/packages/csharp-lib/Enums/MiscReportType.cs +++ b/packages/csharp-lib/Enums/MiscReportType.cs @@ -3,4 +3,8 @@ namespace Wowthing.Lib.Enums; public enum MiscReportType : short { Delve = 1, + LegionEmissaries = 2, + BattleForAzerothEmissaries = 3, + ShadowlandsCallings = 4, } + From c6be0ee284b620c02465a19cfdbdaed05e736a3c Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 12:13:30 -0700 Subject: [PATCH 03/11] Remove some unused styles --- .../components/images/CraftedQualityIcon.svelte | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/apps/frontend/shared/components/images/CraftedQualityIcon.svelte b/apps/frontend/shared/components/images/CraftedQualityIcon.svelte index c761024a8..483bb147b 100644 --- a/apps/frontend/shared/components/images/CraftedQualityIcon.svelte +++ b/apps/frontend/shared/components/images/CraftedQualityIcon.svelte @@ -8,19 +8,6 @@ let { quality, size = 24, ...imageProps }: Props = $props(); - - {#if quality > 0} Date: Wed, 28 Oct 2026 12:15:29 -0700 Subject: [PATCH 04/11] Fix MagicAggregateMiscReportsJob report query --- apps/backend/Jobs/Magic/MagicAggregateMiscReportsJob.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/backend/Jobs/Magic/MagicAggregateMiscReportsJob.cs b/apps/backend/Jobs/Magic/MagicAggregateMiscReportsJob.cs index 5f0694ae5..584cd83e6 100644 --- a/apps/backend/Jobs/Magic/MagicAggregateMiscReportsJob.cs +++ b/apps/backend/Jobs/Magic/MagicAggregateMiscReportsJob.cs @@ -32,7 +32,8 @@ public override async Task Run(string[] data) var dataCounts = new Dictionary(); var reportQuery = Context.MiscReport .AsNoTracking() - .Where(mr => mr.Region == (short)region && + .Where(mr => mr.ReportType == reportType && + mr.Region == (short)region && mr.ExpiresAt > DateTime.UtcNow) .AsAsyncEnumerable(); await foreach (var report in reportQuery) From 1dddb07bb720fa46c63810ca5fce7ac17df24e94 Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 14:36:31 -0700 Subject: [PATCH 05/11] Get emissary data to frontend and make fake holidays for them --- apps/frontend/data/quests.ts | 15 ++++++++++ apps/frontend/enums/holiday.ts | 7 +++++ .../user-home/state/activeHolidays.svelte.ts | 29 +++++++++++++++++-- apps/frontend/user-home/stores/dynamicData.ts | 21 ++++++++++++-- apps/web/Models/Api/ApiDynamicData.cs | 1 + apps/web/Services/MemoryCacheService.cs | 17 +++++++++++ 6 files changed, 85 insertions(+), 5 deletions(-) diff --git a/apps/frontend/data/quests.ts b/apps/frontend/data/quests.ts index a1e2b66c8..2e86e8be0 100644 --- a/apps/frontend/data/quests.ts +++ b/apps/frontend/data/quests.ts @@ -1,3 +1,4 @@ +import { Holiday } from '@/enums/holiday'; import { ItemQuality } from '@/enums/item-quality'; import { QuestStatus } from '@/enums/quest-status'; import { GlobalDailyQuest } from '@/types/data'; @@ -51,6 +52,20 @@ export const dailyQuestLevel: Record = { 8: 60, }; +export const emissaryQuestToHoliday: Record = { + // BfA + 50598: Holiday.EmissaryProudmooreZandalari, + 50599: Holiday.EmissaryProudmooreZandalari, + 50600: Holiday.EmissaryEmbersVoldunai, + 50603: Holiday.EmissaryEmbersVoldunai, + 50601: Holiday.EmissaryStormsTalanjis, + 50602: Holiday.EmissaryStormsTalanjis, + 50605: Holiday.EmissaryLegionHonorbound, + 50606: Holiday.EmissaryLegionHonorbound, + 56119: Holiday.EmissaryWavebladeUnshackled, + 56120: Holiday.EmissaryWavebladeUnshackled, +}; + export const questNameOverride: Record = { 95435: 'Eversong WQs', // Special Assignment: Shade and Claw 92848: 'Eversong WQs', // Special Assignment: The Grand Magister's Drink diff --git a/apps/frontend/enums/holiday.ts b/apps/frontend/enums/holiday.ts index 9bdacef77..ddfd33e60 100644 --- a/apps/frontend/enums/holiday.ts +++ b/apps/frontend/enums/holiday.ts @@ -55,6 +55,13 @@ export enum Holiday { CupPandaria, // MoP CupBrokenIsles, // Legion + // BfA Emissaries + EmissaryProudmooreZandalari, + EmissaryEmbersVoldunai, + EmissaryStormsTalanjis, + EmissaryLegionHonorbound, + EmissaryWavebladeUnshackled, + // Temporary PrepatchMidnight, } diff --git a/apps/frontend/user-home/state/activeHolidays.svelte.ts b/apps/frontend/user-home/state/activeHolidays.svelte.ts index 2fa641e73..e3e51943c 100644 --- a/apps/frontend/user-home/state/activeHolidays.svelte.ts +++ b/apps/frontend/user-home/state/activeHolidays.svelte.ts @@ -1,11 +1,13 @@ -import type { DateTime } from 'luxon'; +import { DateTime } from 'luxon'; +import { holidayIds } from '@/data/holidays'; +import { emissaryQuestToHoliday } from '@/data/quests'; import { timeState } from '@/shared/state/time.svelte'; import { wowthingData } from '@/shared/stores/data'; import { userState } from '@/user-home/state/user'; -import type { StaticDataHoliday } from '@/shared/stores/static/types'; -import { holidayIds } from '@/data/holidays'; +import { dynamicDataStore } from '@/user-home/stores/dynamicData'; import { getNumberKeyedEntries } from '@/utils/get-number-keyed-entries'; +import type { StaticDataHoliday } from '@/shared/stores/static/types'; export type ActiveHoliday = { holiday: StaticDataHoliday; @@ -110,6 +112,27 @@ class ActiveHolidays { } } + // fake holidays for emissary quests + for (const region of allRegions) { + const dynamicData = dynamicDataStore.getCached(region); + for (const emissaryQuests of Object.values(dynamicData.emissaryQuests)) { + const validEmissaryQuests = emissaryQuests.filter( + (eq) => eq.expiresAt > currentTime + ); + for (const emissaryQuest of validEmissaryQuests) { + const emissaryHoliday = emissaryQuestToHoliday[emissaryQuest.questId]; + if (emissaryHoliday) { + activeHolidays[emissaryHoliday] = { + holiday: null, + startDate: emissaryQuest.expiresAt.minus({ days: 1 }), + endDate: emissaryQuest.expiresAt, + soon: false, + }; + } + } + } + } + this.cachedActive[regionMask] = activeHolidays; this.cachedTime[regionMask] = currentTime; diff --git a/apps/frontend/user-home/stores/dynamicData.ts b/apps/frontend/user-home/stores/dynamicData.ts index f3d580faf..e6f0cdf99 100644 --- a/apps/frontend/user-home/stores/dynamicData.ts +++ b/apps/frontend/user-home/stores/dynamicData.ts @@ -3,13 +3,14 @@ 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'; +import type { ApiWorldQuest, ApiWorldQuestReward } from '@/types/world-quests'; interface ApiDynamicData { region: number; delves: [number, string][]; - worldQuests: Record; + emissaryQuests: Record; + worldQuests: Record; } interface ApiWorldQuestJson { @@ -21,6 +22,7 @@ interface ApiWorldQuestJson { class DynamicData { public delves: { poiId: number; story: string }[] = []; + public emissaryQuests: Record = {}; public worldQuests: Record = {}; } @@ -51,6 +53,21 @@ class DynamicDataStore { finalData.delves.push({ poiId, story }); } + // EmissaryQuests + for (const [expansion, emissaryQuests] of getNumberKeyedEntries( + responseData.emissaryQuests + )) { + finalData.emissaryQuests[expansion] = []; + const expansionQuests = finalData.emissaryQuests[expansion]; + + for (const [questId, expiresAt] of emissaryQuests) { + expansionQuests.push({ + questId, + expiresAt: DateTime.fromSeconds(expiresAt), + }); + } + } + // World Quests for (const [zoneId, apiWorldQuests] of getNumberKeyedEntries( responseData.worldQuests diff --git a/apps/web/Models/Api/ApiDynamicData.cs b/apps/web/Models/Api/ApiDynamicData.cs index 8970bad1b..c15e80837 100644 --- a/apps/web/Models/Api/ApiDynamicData.cs +++ b/apps/web/Models/Api/ApiDynamicData.cs @@ -7,5 +7,6 @@ public class ApiDynamicData { public WowRegion Region { get; set; } public object[] Delves { get; set; } + public Dictionary EmissaryQuests { get; set; } public Dictionary WorldQuests { get; set; } } diff --git a/apps/web/Services/MemoryCacheService.cs b/apps/web/Services/MemoryCacheService.cs index 8bd32cb4d..259770e35 100644 --- a/apps/web/Services/MemoryCacheService.cs +++ b/apps/web/Services/MemoryCacheService.cs @@ -22,6 +22,13 @@ public class MemoryCacheService private readonly UserManager _userManager; private readonly WowDbContext _context; + private static readonly (int Expansion, MiscReportType ReportType)[] Emissaries = + [ + (6, MiscReportType.LegionEmissaries), + (7, MiscReportType.BattleForAzerothEmissaries), + (8, MiscReportType.ShadowlandsCallings), + ]; + public MemoryCacheService( CacheService cacheService, IConnectionMultiplexer redis, @@ -165,10 +172,20 @@ public async Task GetDynamicDataForRegion(short region) ? JsonSerializer.Deserialize(delveAggregate.JsonData ?? "[]") : []; + var emissaryQuests = new Dictionary(); + foreach (var emissary in Emissaries) + { + var emissaryAggregate = miscAggregates.FirstOrDefault(ma => ma.ReportType == emissary.ReportType); + emissaryQuests[emissary.Expansion] = emissaryAggregate != null + ? JsonSerializer.Deserialize(emissaryAggregate.JsonData ?? "[]") + : []; + } + return new ApiDynamicData { Region = (WowRegion)region, Delves = delveJson, + EmissaryQuests = emissaryQuests, WorldQuests = worldQuestAggregates.ToGroupedDictionary(wqa => wqa.ZoneId), }; } From 3a213d6538eb05e7fe2b56493e8010b247f5257f Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 14:36:50 -0700 Subject: [PATCH 06/11] Add "[BfA] Emissaries" task - Pass Chore into Chore.customExpiryFunc --- .../home/table/head/HomeTableHeadTasks.svelte | 2 +- apps/frontend/data/icons.ts | 14 +++ .../07-battle-for-azeroth/emissaryQuests.ts | 87 +++++++++++++++++++ .../data/tasks/07-battle-for-azeroth/index.ts | 3 +- .../data/tasks/10-the-war-within/11-0-0.ts | 9 +- apps/frontend/data/tasks/11-midnight/12-0.ts | 10 ++- apps/frontend/data/tasks/11-midnight/12-1.ts | 3 +- .../data/tasks/events/darkmoon-faire.ts | 2 +- apps/frontend/stores/lazy/character.ts | 2 +- apps/frontend/types/tasks.ts | 7 +- .../user-home/state/user/derived.svelte.ts | 2 +- 11 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts diff --git a/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte b/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte index 868642f5e..316058183 100644 --- a/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte +++ b/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte @@ -42,7 +42,7 @@ {@const task = taskMap[taskName] || settingsState.customTaskMap[fullTaskName]} {@const chore = task.chores.find((task) => task?.key === choreName)} {@const customExpiry = chore?.decorationFunc?.( - chore?.customExpiryFunc(userState.general.characters[0], timeState.slowTime) + chore?.customExpiryFunc(chore, userState.general.characters[0], timeState.slowTime) )} = { 'bronze-medal': 'achievement/8888', 'silver-medal': 'achievement/8889', // Mechanized Mackerel 'gold-medal': 'achievement/8890', + + // BfA Alliance + 'faction-7th-legion': 'achievement/12954', + 'faction-order-of-embers': 'achievement/12952', + 'faction-proudmoore-admiralty': 'achievement/12951', + 'faction-storms-wake': 'achievement/12953', + 'faction-waveblade-ankoan': 'achievement/13558', + + // BfA Horde + 'faction-talanjis-expedition': 'achievement/12948', + 'faction-the-honorbound': 'achievement/12957', + 'faction-the-unshackled': 'achievement/13559', + 'faction-voldunai': 'achievement/12949', + 'faction-zandalari-empire': 'achievement/12950', }; export const iconStrings: Record = { diff --git a/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts new file mode 100644 index 000000000..96cf3ccbb --- /dev/null +++ b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts @@ -0,0 +1,87 @@ +import type { DateTime } from 'luxon'; + +import { emissaryQuestToHoliday } from '@/data/quests'; +import { Expansion } from '@/enums/expansion'; +import { Holiday } from '@/enums/holiday'; +import { timeState } from '@/shared/state/time.svelte'; +import { DbResetType } from '@/shared/stores/db/enums'; +import { dynamicDataStore } from '@/user-home/stores/dynamicData'; +import type { Chore, Task } from '@/types/tasks'; + +const customExpiryFunc: Chore['customExpiryFunc'] = (chore, char, scannedAt, questIds) => { + const dynamicData = dynamicDataStore.getCached(char.region); + const activeEmissaryQuests = dynamicData.emissaryQuests[Expansion.BattleForAzeroth] || []; + return activeEmissaryQuests.find( + (eq) => emissaryQuestToHoliday[eq.questId] === chore.requiredHolidays[0] + ).expiresAt; +}; + +const threeDayDecorator = (expires: DateTime) => { + const daysRemaining = expires.diff(timeState.slowTime).toMillis() / 1000 / 86400; + if (daysRemaining < 1) { + return 'decorator-warn'; + } else if (daysRemaining < 2) { + return 'decorator-shrug'; + } else { + return 'decorator-success'; + } +}; + +const emissaryChore: Partial = { + alwaysStarted: true, + overrideNeed: 4, + questReset: DbResetType.Custom, + questResetForced: true, + customExpiryFunc, + decorationFunc: threeDayDecorator, +}; + +export const bfaEmissaryQuests: Task = { + key: 'bfaEmissaryQuests', + name: '[BfA] Emissaries', + shortName: 'BfA', + showSeparate: true, + sumChores: true, + chores: [ + { + key: 'proudmooreZandalari', + name: 'Proudmoore/Zandalari', + icon: ':faction-proudmoore-admiralty: :faction-zandalari-empire:', + questIds: [50599, 50598], + requiredHolidays: [Holiday.EmissaryProudmooreZandalari], + ...emissaryChore, + }, + { + key: 'embersVoldunai', + name: 'Embers/Voldunai', + icon: ':faction-order-of-embers: :faction-voldunai:', + questIds: [50600, 50603], + requiredHolidays: [Holiday.EmissaryEmbersVoldunai], + ...emissaryChore, + }, + { + key: 'stormsTalanjis', + name: "Storm's/Talanji's", + icon: ':faction-storms-wake: :faction-talanjis-expedition:', + questIds: [50601, 50602], + requiredHolidays: [Holiday.EmissaryStormsTalanjis], + ...emissaryChore, + }, + { + key: 'legionHonorbound', + name: 'Legion/Honorbound', + icon: ':faction-7th-legion: :faction-the-honorbound:', + questIds: [50605, 50606], + requiredHolidays: [Holiday.EmissaryLegionHonorbound], + ...emissaryChore, + }, + { + key: 'wavebladeUnshackled', + name: 'Waveblade/Unshackled', + icon: ':faction-waveblade-ankoan: :faction-the-unshackled:', + questIds: [56119, 56120], + requiredHolidays: [Holiday.EmissaryWavebladeUnshackled], + ...emissaryChore, + }, + ], +}; diff --git a/apps/frontend/data/tasks/07-battle-for-azeroth/index.ts b/apps/frontend/data/tasks/07-battle-for-azeroth/index.ts index f355a58af..0a49bc258 100644 --- a/apps/frontend/data/tasks/07-battle-for-azeroth/index.ts +++ b/apps/frontend/data/tasks/07-battle-for-azeroth/index.ts @@ -1,3 +1,4 @@ +import { bfaEmissaryQuests } from './emissaryQuests'; import { bfaNazjatar } from './nazjatar'; -export const bfaTasks = [bfaNazjatar]; +export const bfaTasks = [bfaEmissaryQuests, bfaNazjatar]; diff --git a/apps/frontend/data/tasks/10-the-war-within/11-0-0.ts b/apps/frontend/data/tasks/10-the-war-within/11-0-0.ts index 103714154..b8234c59f 100644 --- a/apps/frontend/data/tasks/10-the-war-within/11-0-0.ts +++ b/apps/frontend/data/tasks/10-the-war-within/11-0-0.ts @@ -35,7 +35,8 @@ export const twwChores11_0_0: Task = { ? [82679] // Archives: Seeking History : [82678], // Archives: The First Disc questReset: DbResetType.Custom, - customExpiryFunc: (char, scannedAt) => customResetPeriod(char, scannedAt, 1002, 3), + customExpiryFunc: (chore, char, scannedAt) => + customResetPeriod(char, scannedAt, 1002, 3), decorationFunc: threeWeekDecorator, }, { @@ -54,7 +55,8 @@ export const twwChores11_0_0: Task = { 82706, // Delves: Worldwide Research ], questReset: DbResetType.Custom, - customExpiryFunc: (char, scannedAt) => customResetPeriod(char, scannedAt, 1003, 3), + customExpiryFunc: (chore, char, scannedAt) => + customResetPeriod(char, scannedAt, 1003, 3), decorationFunc: threeWeekDecorator, }, { @@ -110,7 +112,8 @@ export const twwChores11_0_0: Task = { 82510, // Nerub-ar Palace [H] ], questReset: DbResetType.Custom, - customExpiryFunc: (char, scannedAt) => customResetPeriod(char, scannedAt, 1001, 3), + customExpiryFunc: (chore, char, scannedAt) => + customResetPeriod(char, scannedAt, 1001, 3), decorationFunc: threeWeekDecorator, }, { diff --git a/apps/frontend/data/tasks/11-midnight/12-0.ts b/apps/frontend/data/tasks/11-midnight/12-0.ts index 5646899d7..1c2d4a95d 100644 --- a/apps/frontend/data/tasks/11-midnight/12-0.ts +++ b/apps/frontend/data/tasks/11-midnight/12-0.ts @@ -76,7 +76,12 @@ export const specialAssignmentFunc = (unlocks: number[], index: number, isQuest: }; }; -export const specialAssignmentExpiry: Chore['customExpiryFunc'] = (char, scannedAt, questIds) => { +export const specialAssignmentExpiry: Chore['customExpiryFunc'] = ( + chore, + char, + scannedAt, + questIds +) => { const allWorldQuests = dynamicDataStore.getCachedQuests(char.region); const findQuestId = specialAssignmentQuestToUnlock[questIds[0]] || questIds[0]; const worldQuest = allWorldQuests.find((wq) => wq.questId === findQuestId); @@ -97,7 +102,8 @@ export const midChores12_0: Task = { questResetForced: true, showQuestName: true, sumObjectives: true, - customExpiryFunc: (char, scannedAt) => customResetPeriod(char, scannedAt, 1079, 2), + customExpiryFunc: (chore, char, scannedAt) => + customResetPeriod(char, scannedAt, 1079, 2), decorationFunc: twoWeekDecorator, questIds: (char) => char.level < 90 diff --git a/apps/frontend/data/tasks/11-midnight/12-1.ts b/apps/frontend/data/tasks/11-midnight/12-1.ts index e357876ef..041da7e62 100644 --- a/apps/frontend/data/tasks/11-midnight/12-1.ts +++ b/apps/frontend/data/tasks/11-midnight/12-1.ts @@ -46,7 +46,8 @@ export const midChores12_1: Task = { questReset: DbResetType.Custom, questResetForced: true, questIds: [98172], - customExpiryFunc: (char, scannedAt) => customResetPeriod(char, scannedAt, 1078, 2), + customExpiryFunc: (chore, char, scannedAt) => + customResetPeriod(char, scannedAt, 1078, 2), decorationFunc: twoWeekDecorator, }, { diff --git a/apps/frontend/data/tasks/events/darkmoon-faire.ts b/apps/frontend/data/tasks/events/darkmoon-faire.ts index f27b1f633..bdbb6c343 100644 --- a/apps/frontend/data/tasks/events/darkmoon-faire.ts +++ b/apps/frontend/data/tasks/events/darkmoon-faire.ts @@ -4,7 +4,7 @@ import { DbResetType } from '@/shared/stores/db/enums'; import { userStore } from '@/stores'; import type { Chore, Task } from '@/types/tasks'; -const customExpiryFunc: Chore['customExpiryFunc'] = (char, scannedAt) => { +const customExpiryFunc: Chore['customExpiryFunc'] = (chore, char, scannedAt) => { const scannedPeriod = userStore.getPeriodForCharacter2(scannedAt, char); return scannedPeriod.endTime.plus({ days: 3 }); }; diff --git a/apps/frontend/stores/lazy/character.ts b/apps/frontend/stores/lazy/character.ts index 555f419d2..7b8cc8ede 100644 --- a/apps/frontend/stores/lazy/character.ts +++ b/apps/frontend/stores/lazy/character.ts @@ -877,7 +877,7 @@ function getExpiry(choreTask: Chore, character: Character, charScanned: DateTime if (choreTask.questReset === DbResetType.Weekly) { expiresAt = getNextWeeklyResetFromTime(charScanned, character.realm?.region || Region.US); } else if (choreTask.questReset === DbResetType.Custom) { - expiresAt = choreTask.customExpiryFunc(character, charScanned); + expiresAt = choreTask.customExpiryFunc(choreTask, character, charScanned); } else { expiresAt = getNextDailyResetFromTime(charScanned, character.realm?.region || Region.US); } diff --git a/apps/frontend/types/tasks.ts b/apps/frontend/types/tasks.ts index f1d408cfe..f7f9bc1d6 100644 --- a/apps/frontend/types/tasks.ts +++ b/apps/frontend/types/tasks.ts @@ -54,7 +54,12 @@ export type Chore = { /** * Function to generate a custom expiry time */ - customExpiryFunc?: (char: Character, scannedAt: DateTime, questIds?: number[]) => DateTime; + customExpiryFunc?: ( + chore: Chore, + char: Character, + scannedAt: DateTime, + questIds?: number[] + ) => DateTime; /** * Function to ???? */ diff --git a/apps/frontend/user-home/state/user/derived.svelte.ts b/apps/frontend/user-home/state/user/derived.svelte.ts index 0b3bb50fc..9709671b3 100644 --- a/apps/frontend/user-home/state/user/derived.svelte.ts +++ b/apps/frontend/user-home/state/user/derived.svelte.ts @@ -583,7 +583,7 @@ export class DataUserDerived { : chore.questIds; if (choreReset === DbResetType.Custom) { - expiresAt = chore.customExpiryFunc(character, charScanned, questIds); + expiresAt = chore.customExpiryFunc(chore, character, charScanned, questIds); } for (const questId of questIds) { From 3da7b333f1baa86614a877855bd965e395fd88f3 Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 20:30:06 -0700 Subject: [PATCH 07/11] Start moving reputation data to a central place --- apps/frontend/data/icons.ts | 14 -- apps/frontend/data/quests.ts | 14 -- apps/frontend/data/reputation.ts | 179 ++++++++++++++++++ .../07-battle-for-azeroth/emissaryQuests.ts | 49 +++-- apps/frontend/enums/holiday.ts | 13 ++ apps/frontend/enums/reputation.ts | 46 +++++ .../components/parsed-text/ParsedText.svelte | 8 + apps/frontend/types/reputation-data.ts | 7 + .../user-home/state/activeHolidays.svelte.ts | 2 +- 9 files changed, 287 insertions(+), 45 deletions(-) create mode 100644 apps/frontend/enums/reputation.ts create mode 100644 apps/frontend/types/reputation-data.ts diff --git a/apps/frontend/data/icons.ts b/apps/frontend/data/icons.ts index dfa4c9909..961def3ed 100644 --- a/apps/frontend/data/icons.ts +++ b/apps/frontend/data/icons.ts @@ -36,20 +36,6 @@ export const imageStrings: Record = { 'bronze-medal': 'achievement/8888', 'silver-medal': 'achievement/8889', // Mechanized Mackerel 'gold-medal': 'achievement/8890', - - // BfA Alliance - 'faction-7th-legion': 'achievement/12954', - 'faction-order-of-embers': 'achievement/12952', - 'faction-proudmoore-admiralty': 'achievement/12951', - 'faction-storms-wake': 'achievement/12953', - 'faction-waveblade-ankoan': 'achievement/13558', - - // BfA Horde - 'faction-talanjis-expedition': 'achievement/12948', - 'faction-the-honorbound': 'achievement/12957', - 'faction-the-unshackled': 'achievement/13559', - 'faction-voldunai': 'achievement/12949', - 'faction-zandalari-empire': 'achievement/12950', }; export const iconStrings: Record = { diff --git a/apps/frontend/data/quests.ts b/apps/frontend/data/quests.ts index 2e86e8be0..232491591 100644 --- a/apps/frontend/data/quests.ts +++ b/apps/frontend/data/quests.ts @@ -52,20 +52,6 @@ export const dailyQuestLevel: Record = { 8: 60, }; -export const emissaryQuestToHoliday: Record = { - // BfA - 50598: Holiday.EmissaryProudmooreZandalari, - 50599: Holiday.EmissaryProudmooreZandalari, - 50600: Holiday.EmissaryEmbersVoldunai, - 50603: Holiday.EmissaryEmbersVoldunai, - 50601: Holiday.EmissaryStormsTalanjis, - 50602: Holiday.EmissaryStormsTalanjis, - 50605: Holiday.EmissaryLegionHonorbound, - 50606: Holiday.EmissaryLegionHonorbound, - 56119: Holiday.EmissaryWavebladeUnshackled, - 56120: Holiday.EmissaryWavebladeUnshackled, -}; - export const questNameOverride: Record = { 95435: 'Eversong WQs', // Special Assignment: Shade and Claw 92848: 'Eversong WQs', // Special Assignment: The Grand Magister's Drink diff --git a/apps/frontend/data/reputation.ts b/apps/frontend/data/reputation.ts index 54fcc85bc..a5003f006 100644 --- a/apps/frontend/data/reputation.ts +++ b/apps/frontend/data/reputation.ts @@ -1,3 +1,182 @@ +import { Holiday } from '@/enums/holiday'; +import { Reputation } from '@/enums/reputation'; +import type { ReputationData } from '@/types/reputation-data'; +import { groupBy } from 'lodash'; + +export const reputationMap: Partial> = { + [Reputation.LegionHighmountainTribe]: { + emissaryHoliday: Holiday.EmissaryHighmountainTribes, + emissaryQuestId: 42233, + icon: 'item/146944', // Insignia + }, + [Reputation.LegionTheNightfallen]: { + emissaryHoliday: Holiday.EmissaryNightfallen, + emissaryQuestId: 42421, + icon: 'item/146946', // Insignia + }, + [Reputation.LegionDreamweavers]: { + emissaryHoliday: Holiday.EmissaryDreamweavers, + emissaryQuestId: 42170, + icon: 'item/146942', // Insignia + }, + [Reputation.LegionTheWardens]: { + emissaryHoliday: Holiday.EmissaryWardens, + emissaryQuestId: 42422, + icon: 'item/146945', // Insignia + }, + [Reputation.LegionCourtOfFarondis]: { + emissaryHoliday: Holiday.EmissaryCourtFarondis, + emissaryQuestId: 42420, + icon: 'item/146943', // Insignia + }, + [Reputation.LegionValarjar]: { + emissaryHoliday: Holiday.EmissaryValarjar, + emissaryQuestId: 42234, + icon: 'item/146941', // Insignia + }, + [Reputation.LegionConjurerMargoss]: { + icon: 'item/138777', // Drowned Mana + }, + [Reputation.LegionTalonsVengeance]: { + icon: 'item/142369', // Ivory Hawkstrider + }, + [Reputation.LegionArmiesOfLegionfall]: { + emissaryHoliday: Holiday.EmissaryArmiesLegionfall, + emissaryQuestId: 48641, + icon: 'item/146950', // Insignia + }, + [Reputation.LegionIlyssiaOfTheWaters]: { + icon: 'item/146848', // Fragmented Enchantment + }, + [Reputation.LegionKeeperRaynae]: { + icon: 'item/146959', // Corrupted Globule + }, + [Reputation.LegionAkuleRiverhorn]: { + icon: 'item/146960', // Ancient Totem Fragment + }, + [Reputation.LegionCorbyn]: { + icon: 'item/146961', // Shiny Bauble, + }, + [Reputation.LegionShaleth]: { + icon: 'item/146962', // Golden Minnow + }, + [Reputation.LegionImpus]: { + icon: 'item/146963', // Desecrated Seaweed + }, + [Reputation.LegionChromie]: { + icon: 'achievement/11941', + }, + [Reputation.LegionArmyOfTheLight]: { + emissaryHoliday: Holiday.EmissaryArmyLight, + emissaryQuestId: 48639, + icon: 'item/152957', // Insignia + }, + [Reputation.LegionArgussianReach]: { + emissaryHoliday: Holiday.EmissaryArgussianReach, + emissaryQuestId: 48642, + icon: 'item/152960', // Insignia + }, + + [Reputation.BfaZandalariEmpire]: { + emissaryHoliday: Holiday.EmissaryProudmooreZandalari, + emissaryQuestId: 50599, + icon: 'item/238818', // Commendation + }, + [Reputation.BfaTalanjisExpedition]: { + emissaryHoliday: Holiday.EmissaryStormsTalanjis, + emissaryQuestId: 50602, + icon: 'item/238816', // Commendation + }, + [Reputation.BfaTheHonorbound]: { + emissaryHoliday: Holiday.EmissaryLegionHonorbound, + emissaryQuestId: 50606, + icon: 'item/238819', // Commendation + }, + [Reputation.BfaVoldunai]: { + emissaryHoliday: Holiday.EmissaryEmbersVoldunai, + emissaryQuestId: 50603, + icon: 'item/238817', // Commendation + }, + [Reputation.Bfa7thLegion]: { + emissaryHoliday: Holiday.EmissaryLegionHonorbound, + emissaryQuestId: 50605, + icon: 'item/238806', // Commendation + }, + [Reputation.BfaProudmooreAdmiralty]: { + emissaryHoliday: Holiday.EmissaryProudmooreZandalari, + emissaryQuestId: 50598, + icon: 'item/238804', // Commendation + }, + [Reputation.BfaOrderOfEmbers]: { + emissaryHoliday: Holiday.EmissaryEmbersVoldunai, + emissaryQuestId: 50600, + icon: 'item/238803', // Commendation + }, + [Reputation.BfaStormsWake]: { + emissaryHoliday: Holiday.EmissaryStormsTalanjis, + emissaryQuestId: 50601, + icon: 'item/238805', // Commendation + }, + [Reputation.BfaTortollanSeekers]: { + emissaryHoliday: Holiday.EmissaryTortollans, + emissaryQuestId: 50604, + icon: 'item/238822', // Commendation + }, + [Reputation.BfAChampionsOfAzeroth]: { + emissaryHoliday: Holiday.EmissaryChampionsAzeroth, + emissaryQuestId: 50562, + icon: 'item/238821', // Commendation + }, + [Reputation.BfaTheUnshackled]: { + emissaryHoliday: Holiday.EmissaryWavebladeUnshackled, + emissaryQuestId: 56120, + icon: 'item/238820', // Commendation + }, + [Reputation.BfaHunterAkana]: { + icon: 'achievement/13758', + }, + [Reputation.BfaFarseerOri]: { + icon: 'achievement/13760', + }, + [Reputation.BfaBladesmanInowari]: { + icon: 'achievement/13759', + }, + [Reputation.BfaPoenGillbrack]: { + icon: 'achievement/13756', + }, + [Reputation.BfaNeriSharpfin]: { + icon: 'achievement/13750', + }, + [Reputation.BfaVimBrineheart]: { + icon: 'achievement/13757', + }, + [Reputation.BfaRustboltResistance]: { + icon: 'item/238823', // Commendation + }, + [Reputation.BfaWavebladeAnkoan]: { + emissaryHoliday: Holiday.EmissaryWavebladeUnshackled, + emissaryQuestId: 56119, + icon: 'item/238809', // Commendation + }, + [Reputation.BfaRajani]: { + icon: 'item/238826', // Commendation + }, + [Reputation.BfaUldumAccord]: { + icon: 'item/238827', // Commendation + }, +}; + +export const emissaryQuestToHoliday: Record = Object.fromEntries( + Object.values(reputationMap) + .filter((rep) => !!rep.emissaryQuestId) + .map((rep) => [rep.emissaryQuestId, rep.emissaryHoliday]) +); + +export const holidayToReputations: Partial> = groupBy( + Object.values(reputationMap).filter((rep) => !!rep.emissaryHoliday), + (rep) => rep.emissaryHoliday +); + export const contractAuras: Record = { 384317: [2544, 1], // Contract: Artisan's Consortium 1 384320: [2544, 2], // Contract: Artisan's Consortium 2 diff --git a/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts index 96cf3ccbb..3945bae14 100644 --- a/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts +++ b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts @@ -1,6 +1,6 @@ import type { DateTime } from 'luxon'; -import { emissaryQuestToHoliday } from '@/data/quests'; +import { emissaryQuestToHoliday, holidayToReputations } from '@/data/reputation'; import { Expansion } from '@/enums/expansion'; import { Holiday } from '@/enums/holiday'; import { timeState } from '@/shared/state/time.svelte'; @@ -36,6 +36,14 @@ const emissaryChore: Partial = { decorationFunc: threeDayDecorator, }; +const holidayData = (holiday: Holiday): Partial => { + const reputations = holidayToReputations[holiday] || []; + return { + questIds: reputations.map((rep) => rep.emissaryQuestId), + requiredHolidays: [holiday], + }; +}; + export const bfaEmissaryQuests: Task = { key: 'bfaEmissaryQuests', name: '[BfA] Emissaries', @@ -43,44 +51,53 @@ export const bfaEmissaryQuests: Task = { showSeparate: true, sumChores: true, chores: [ + { + key: 'champions', + name: 'Champions of Azeroth', + icon: '{factionIcon:BfaChampionsOfAzeroth}', + ...holidayData(Holiday.EmissaryChampionsAzeroth), + ...emissaryChore, + }, + { + key: 'tortollans', + name: 'Tortollans', + icon: '{factionIcon:BfaTortollans}', + ...holidayData(Holiday.EmissaryTortollans), + ...emissaryChore, + }, { key: 'proudmooreZandalari', name: 'Proudmoore/Zandalari', - icon: ':faction-proudmoore-admiralty: :faction-zandalari-empire:', - questIds: [50599, 50598], - requiredHolidays: [Holiday.EmissaryProudmooreZandalari], + icon: '{factionIcon:BfaProudmooreAdmiralty} {factionIcon:BfaZandalariEmpire}', + ...holidayData(Holiday.EmissaryProudmooreZandalari), ...emissaryChore, }, { key: 'embersVoldunai', name: 'Embers/Voldunai', - icon: ':faction-order-of-embers: :faction-voldunai:', - questIds: [50600, 50603], - requiredHolidays: [Holiday.EmissaryEmbersVoldunai], + icon: '{factionIcon:BfaOrderOfEmbers} {factionIcon:BfaVoldunai}', + ...holidayData(Holiday.EmissaryEmbersVoldunai), ...emissaryChore, }, { key: 'stormsTalanjis', name: "Storm's/Talanji's", - icon: ':faction-storms-wake: :faction-talanjis-expedition:', - questIds: [50601, 50602], - requiredHolidays: [Holiday.EmissaryStormsTalanjis], + icon: '{factionIcon:BfaStormsWake} {factionIcon:BfaTalanjisExpedition}', + ...holidayData(Holiday.EmissaryStormsTalanjis), ...emissaryChore, }, { key: 'legionHonorbound', name: 'Legion/Honorbound', - icon: ':faction-7th-legion: :faction-the-honorbound:', - questIds: [50605, 50606], - requiredHolidays: [Holiday.EmissaryLegionHonorbound], + icon: '{factionIcon:Bfa7thLegion} {factionIcon:BfaTheHonorbound}', + ...holidayData(Holiday.EmissaryLegionHonorbound), ...emissaryChore, }, { key: 'wavebladeUnshackled', name: 'Waveblade/Unshackled', - icon: ':faction-waveblade-ankoan: :faction-the-unshackled:', - questIds: [56119, 56120], - requiredHolidays: [Holiday.EmissaryWavebladeUnshackled], + icon: '{factionIcon:BfaWavebladeAnkoan} {factionIcon:BfaTheUnshackled}', + ...holidayData(Holiday.EmissaryWavebladeUnshackled), ...emissaryChore, }, ], diff --git a/apps/frontend/enums/holiday.ts b/apps/frontend/enums/holiday.ts index ddfd33e60..e826d1265 100644 --- a/apps/frontend/enums/holiday.ts +++ b/apps/frontend/enums/holiday.ts @@ -55,7 +55,20 @@ export enum Holiday { CupPandaria, // MoP CupBrokenIsles, // Legion + // Legion Emissaries + EmissaryCourtFarondis, + EmissaryDreamweavers, + EmissaryHighmountainTribes, + EmissaryNightfallen, + EmissaryValarjar, + EmissaryWardens, + EmissaryArmiesLegionfall, + EmissaryArgussianReach, + EmissaryArmyLight, + // BfA Emissaries + EmissaryChampionsAzeroth, + EmissaryTortollans, EmissaryProudmooreZandalari, EmissaryEmbersVoldunai, EmissaryStormsTalanjis, diff --git a/apps/frontend/enums/reputation.ts b/apps/frontend/enums/reputation.ts new file mode 100644 index 000000000..c885b9558 --- /dev/null +++ b/apps/frontend/enums/reputation.ts @@ -0,0 +1,46 @@ +export enum Reputation { + LegionHighmountainTribe = 1828, + LegionTheNightfallen = 1859, + LegionDreamweavers = 1883, + LegionTheWardens = 1894, + LegionCourtOfFarondis = 1900, + LegionValarjar = 1948, + LegionConjurerMargoss = 1975, + LegionTalonsVengeance = 2018, + LegionArmiesOfLegionfall = 2045, + LegionIlyssiaOfTheWaters = 2097, + LegionKeeperRaynae = 2098, + LegionAkuleRiverhorn = 2099, + LegionCorbyn = 2100, + LegionShaleth = 2101, + LegionImpus = 2102, + LegionChromie = 2135, + LegionArmyOfTheLight = 2165, + LegionArgussianReach = 2170, + + BfaZandalariEmpire = 2103, + BfaTalanjisExpedition = 2156, + BfaTheHonorbound = 2157, + BfaVoldunai = 2158, + Bfa7thLegion = 2159, + BfaProudmooreAdmiralty = 2160, + BfaOrderOfEmbers = 2161, + BfaStormsWake = 2162, + BfaTortollanSeekers = 2163, + BfAChampionsOfAzeroth = 2164, + BfaTheUnshackled = 2373, + BfaHunterAkana = 2375, + BfaFarseerOri = 2376, + BfaBladesmanInowari = 2377, + BfaPoenGillbrack = 2388, + BfaNeriSharpfin = 2389, + BfaVimBrineheart = 2390, + BfaRustboltResistance = 2391, + BfaHoneybackHive = 2395, + BfaHoneybackDrone = 2396, + BfaHoneybackHivemother = 2397, + BfaHoneybackHarvester = 2398, + BfaWavebladeAnkoan = 2400, + BfaRajani = 2415, + BfaUldumAccord = 2417, +} diff --git a/apps/frontend/shared/components/parsed-text/ParsedText.svelte b/apps/frontend/shared/components/parsed-text/ParsedText.svelte index 67401f2f8..2c63b34ee 100644 --- a/apps/frontend/shared/components/parsed-text/ParsedText.svelte +++ b/apps/frontend/shared/components/parsed-text/ParsedText.svelte @@ -2,8 +2,10 @@ import { afterUpdate, mount } from 'svelte'; import { iconStrings, imageStrings } from '@/data/icons'; + import { reputationMap } from '@/data/reputation'; import { Faction } from '@/enums/faction'; import { PlayableClass, PlayableClassMask } from '@/enums/playable-class'; + import { Reputation } from '@/enums/reputation'; import { RewardReputation } from '@/enums/reward-reputation'; import { iconLibrary, aliasedIcons, uiIcons } from '@/shared/icons'; import { wowthingData } from '@/shared/stores/data'; @@ -43,6 +45,12 @@ return rep?.name || `Faction #${factionId}`; }); + // {factionIcon:factionId} + html = html.replaceAll(/\{factionIcon:(\w+)\}/g, (_, factionEnumString: string) => { + const reputation = Reputation[factionEnumString as keyof typeof Reputation]; + return ``; + }); + // {reputation:amount|factionId} html = html.replaceAll( /\{reputation:(\d+)\|(\d+)\}/g, diff --git a/apps/frontend/types/reputation-data.ts b/apps/frontend/types/reputation-data.ts new file mode 100644 index 000000000..e41510427 --- /dev/null +++ b/apps/frontend/types/reputation-data.ts @@ -0,0 +1,7 @@ +import type { Holiday } from '@/enums/holiday'; + +export type ReputationData = { + emissaryHoliday?: Holiday; + emissaryQuestId?: number; + icon: string; +}; diff --git a/apps/frontend/user-home/state/activeHolidays.svelte.ts b/apps/frontend/user-home/state/activeHolidays.svelte.ts index e3e51943c..2fb60b239 100644 --- a/apps/frontend/user-home/state/activeHolidays.svelte.ts +++ b/apps/frontend/user-home/state/activeHolidays.svelte.ts @@ -1,7 +1,7 @@ import { DateTime } from 'luxon'; import { holidayIds } from '@/data/holidays'; -import { emissaryQuestToHoliday } from '@/data/quests'; +import { emissaryQuestToHoliday } from '@/data/reputation'; import { timeState } from '@/shared/state/time.svelte'; import { wowthingData } from '@/shared/stores/data'; import { userState } from '@/user-home/state/user'; From 26d0251870b01016fbfaa2f724279d9965d336a2 Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 22:26:10 -0700 Subject: [PATCH 08/11] Add "[Legion] Emissaries" task --- .../data/tasks/06-legion/emissaryQuests.ts | 67 +++++++++++++++++++ apps/frontend/data/tasks/06-legion/index.ts | 3 + .../07-battle-for-azeroth/emissaryQuests.ts | 67 +++---------------- apps/frontend/data/tasks/index.ts | 4 +- apps/frontend/data/tasks/utils.ts | 40 +++++++++++ 5 files changed, 122 insertions(+), 59 deletions(-) create mode 100644 apps/frontend/data/tasks/06-legion/emissaryQuests.ts create mode 100644 apps/frontend/data/tasks/06-legion/index.ts create mode 100644 apps/frontend/data/tasks/utils.ts diff --git a/apps/frontend/data/tasks/06-legion/emissaryQuests.ts b/apps/frontend/data/tasks/06-legion/emissaryQuests.ts new file mode 100644 index 000000000..5f867a688 --- /dev/null +++ b/apps/frontend/data/tasks/06-legion/emissaryQuests.ts @@ -0,0 +1,67 @@ +import { Holiday } from '@/enums/holiday'; +import type { Task } from '@/types/tasks'; +import { emissaryChoreData } from '../utils'; + +export const legionEmissaryQuests: Task = { + key: 'legionEmissaryQuests', + name: '[Legion] Emissaries', + shortName: 'Leg', + showSeparate: true, + sumChores: true, + chores: [ + { + key: 'courtFarondis', + name: 'Court of Farondis', + icon: '{factionIcon:LegionCourtOfFarondis}', + ...emissaryChoreData(Holiday.EmissaryCourtFarondis), + }, + { + key: 'dreamweavers', + name: 'Dreamweavers', + icon: '{factionIcon:LegionDreamweavers}', + ...emissaryChoreData(Holiday.EmissaryDreamweavers), + }, + { + key: 'highmountain', + name: 'Highmountain Tribes', + icon: '{factionIcon:LegionHighmountainTribes}', + ...emissaryChoreData(Holiday.EmissaryHighmountainTribes), + }, + { + key: 'nightfallen', + name: 'Nightfallen', + icon: '{factionIcon:LegionNightfallen}', + ...emissaryChoreData(Holiday.EmissaryNightfallen), + }, + { + key: 'valarjar', + name: 'Valarjar', + icon: '{factionIcon:LegionValarjar}', + ...emissaryChoreData(Holiday.EmissaryValarjar), + }, + { + key: 'wardens', + name: 'Wardens', + icon: '{factionIcon:LegionWardens}', + ...emissaryChoreData(Holiday.EmissaryWardens), + }, + { + key: 'armiesLegionfall', + name: 'Armies of Legionfall', + icon: '{factionIcon:LegionArmiesOfLegionfall}', + ...emissaryChoreData(Holiday.EmissaryArmiesLegionfall), + }, + { + key: 'argussianReach', + name: 'Argussian Reach', + icon: '{factionIcon:LegionArgussianReach}', + ...emissaryChoreData(Holiday.EmissaryArgussianReach), + }, + { + key: 'armyLight', + name: 'Army of Light', + icon: '{factionIcon:LegionArmyOfLight}', + ...emissaryChoreData(Holiday.EmissaryArmyLight), + }, + ], +}; diff --git a/apps/frontend/data/tasks/06-legion/index.ts b/apps/frontend/data/tasks/06-legion/index.ts new file mode 100644 index 000000000..329bda293 --- /dev/null +++ b/apps/frontend/data/tasks/06-legion/index.ts @@ -0,0 +1,3 @@ +import { legionEmissaryQuests } from './emissaryQuests'; + +export const legionTasks = [legionEmissaryQuests]; diff --git a/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts index 3945bae14..491f74646 100644 --- a/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts +++ b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts @@ -1,48 +1,6 @@ -import type { DateTime } from 'luxon'; - -import { emissaryQuestToHoliday, holidayToReputations } from '@/data/reputation'; -import { Expansion } from '@/enums/expansion'; import { Holiday } from '@/enums/holiday'; -import { timeState } from '@/shared/state/time.svelte'; -import { DbResetType } from '@/shared/stores/db/enums'; -import { dynamicDataStore } from '@/user-home/stores/dynamicData'; -import type { Chore, Task } from '@/types/tasks'; - -const customExpiryFunc: Chore['customExpiryFunc'] = (chore, char, scannedAt, questIds) => { - const dynamicData = dynamicDataStore.getCached(char.region); - const activeEmissaryQuests = dynamicData.emissaryQuests[Expansion.BattleForAzeroth] || []; - return activeEmissaryQuests.find( - (eq) => emissaryQuestToHoliday[eq.questId] === chore.requiredHolidays[0] - ).expiresAt; -}; - -const threeDayDecorator = (expires: DateTime) => { - const daysRemaining = expires.diff(timeState.slowTime).toMillis() / 1000 / 86400; - if (daysRemaining < 1) { - return 'decorator-warn'; - } else if (daysRemaining < 2) { - return 'decorator-shrug'; - } else { - return 'decorator-success'; - } -}; - -const emissaryChore: Partial = { - alwaysStarted: true, - overrideNeed: 4, - questReset: DbResetType.Custom, - questResetForced: true, - customExpiryFunc, - decorationFunc: threeDayDecorator, -}; - -const holidayData = (holiday: Holiday): Partial => { - const reputations = holidayToReputations[holiday] || []; - return { - questIds: reputations.map((rep) => rep.emissaryQuestId), - requiredHolidays: [holiday], - }; -}; +import type { Task } from '@/types/tasks'; +import { emissaryChoreData } from '../utils'; export const bfaEmissaryQuests: Task = { key: 'bfaEmissaryQuests', @@ -55,50 +13,43 @@ export const bfaEmissaryQuests: Task = { key: 'champions', name: 'Champions of Azeroth', icon: '{factionIcon:BfaChampionsOfAzeroth}', - ...holidayData(Holiday.EmissaryChampionsAzeroth), - ...emissaryChore, + ...emissaryChoreData(Holiday.EmissaryChampionsAzeroth), }, { key: 'tortollans', name: 'Tortollans', icon: '{factionIcon:BfaTortollans}', - ...holidayData(Holiday.EmissaryTortollans), - ...emissaryChore, + ...emissaryChoreData(Holiday.EmissaryTortollans), }, { key: 'proudmooreZandalari', name: 'Proudmoore/Zandalari', icon: '{factionIcon:BfaProudmooreAdmiralty} {factionIcon:BfaZandalariEmpire}', - ...holidayData(Holiday.EmissaryProudmooreZandalari), - ...emissaryChore, + ...emissaryChoreData(Holiday.EmissaryProudmooreZandalari), }, { key: 'embersVoldunai', name: 'Embers/Voldunai', icon: '{factionIcon:BfaOrderOfEmbers} {factionIcon:BfaVoldunai}', - ...holidayData(Holiday.EmissaryEmbersVoldunai), - ...emissaryChore, + ...emissaryChoreData(Holiday.EmissaryEmbersVoldunai), }, { key: 'stormsTalanjis', name: "Storm's/Talanji's", icon: '{factionIcon:BfaStormsWake} {factionIcon:BfaTalanjisExpedition}', - ...holidayData(Holiday.EmissaryStormsTalanjis), - ...emissaryChore, + ...emissaryChoreData(Holiday.EmissaryStormsTalanjis), }, { key: 'legionHonorbound', name: 'Legion/Honorbound', icon: '{factionIcon:Bfa7thLegion} {factionIcon:BfaTheHonorbound}', - ...holidayData(Holiday.EmissaryLegionHonorbound), - ...emissaryChore, + ...emissaryChoreData(Holiday.EmissaryLegionHonorbound), }, { key: 'wavebladeUnshackled', name: 'Waveblade/Unshackled', icon: '{factionIcon:BfaWavebladeAnkoan} {factionIcon:BfaTheUnshackled}', - ...holidayData(Holiday.EmissaryWavebladeUnshackled), - ...emissaryChore, + ...emissaryChoreData(Holiday.EmissaryWavebladeUnshackled), }, ], }; diff --git a/apps/frontend/data/tasks/index.ts b/apps/frontend/data/tasks/index.ts index 78585dd77..b45c47faf 100644 --- a/apps/frontend/data/tasks/index.ts +++ b/apps/frontend/data/tasks/index.ts @@ -1,4 +1,5 @@ import { wodTasks } from './05-warlords-of-draenor'; +import { legionTasks } from './06-legion'; import { bfaTasks } from './07-battle-for-azeroth'; import { slTasks } from './08-shadowlands'; import { dfTasks } from './09-dragonflight'; @@ -11,8 +12,9 @@ import type { Task } from '@/types/tasks'; export const taskList: Task[] = [ ...eventTasks, ...pvpTasks, - ...bfaTasks, ...wodTasks, + ...legionTasks, + ...bfaTasks, ...slTasks, ...dfTasks, ...twwTasks, diff --git a/apps/frontend/data/tasks/utils.ts b/apps/frontend/data/tasks/utils.ts new file mode 100644 index 000000000..e3dab70a0 --- /dev/null +++ b/apps/frontend/data/tasks/utils.ts @@ -0,0 +1,40 @@ +import type { DateTime } from 'luxon'; + +import { emissaryQuestToHoliday, holidayToReputations } from '@/data/reputation'; +import { Expansion } from '@/enums/expansion'; +import { timeState } from '@/shared/state/time.svelte'; +import { DbResetType } from '@/shared/stores/db/enums'; +import { dynamicDataStore } from '@/user-home/stores/dynamicData'; +import type { Holiday } from '@/enums/holiday'; +import type { Chore } from '@/types/tasks'; + +export const threeDayDecorator = (expires: DateTime) => { + const daysRemaining = expires.diff(timeState.slowTime).toMillis() / 1000 / 86400; + if (daysRemaining < 1) { + return 'decorator-warn'; + } else if (daysRemaining < 2) { + return 'decorator-shrug'; + } else { + return 'decorator-success'; + } +}; + +export const emissaryChoreData = (holiday: Holiday): Partial => { + const reputations = holidayToReputations[holiday] || []; + return { + alwaysStarted: true, + overrideNeed: 4, + questReset: DbResetType.Custom, + questResetForced: true, + questIds: reputations.map((rep) => rep.emissaryQuestId), + requiredHolidays: [holiday], + customExpiryFunc: (chore, char, scannedAt, questIds) => { + const dynamicData = dynamicDataStore.getCached(char.region); + const activeEmissaryQuests = Object.values(dynamicData.emissaryQuests).flat(); + return activeEmissaryQuests.find( + (eq) => emissaryQuestToHoliday[eq.questId] === chore.requiredHolidays[0] + )?.expiresAt; + }, + decorationFunc: threeDayDecorator, + }; +}; From 673cd828990575569004bb888acd961a449060d6 Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 22:28:25 -0700 Subject: [PATCH 09/11] Add Chore.noTooltipIcon --- apps/frontend/components/tooltips/task/TooltipTaskRow.svelte | 2 +- apps/frontend/data/tasks/utils.ts | 1 + apps/frontend/types/tasks.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte b/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte index 806fe20f0..c75e4fa2a 100644 --- a/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte +++ b/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte @@ -170,7 +170,7 @@ charTaskChore.status ]}" > - {#if chore?.icon} + {#if chore?.icon && !chore.noTooltipIcon} {#if typeof chore.icon === 'string'} {:else} diff --git a/apps/frontend/data/tasks/utils.ts b/apps/frontend/data/tasks/utils.ts index e3dab70a0..c21eb0d83 100644 --- a/apps/frontend/data/tasks/utils.ts +++ b/apps/frontend/data/tasks/utils.ts @@ -23,6 +23,7 @@ export const emissaryChoreData = (holiday: Holiday): Partial => { const reputations = holidayToReputations[holiday] || []; return { alwaysStarted: true, + noTooltipIcon: true, overrideNeed: 4, questReset: DbResetType.Custom, questResetForced: true, diff --git a/apps/frontend/types/tasks.ts b/apps/frontend/types/tasks.ts index f7f9bc1d6..8d90a0465 100644 --- a/apps/frontend/types/tasks.ts +++ b/apps/frontend/types/tasks.ts @@ -28,6 +28,7 @@ export type Chore = { maximumLevel?: number; minimumLevel?: number; noAlone?: boolean; + noTooltipIcon?: boolean; overrideNeed?: number; questCount?: number; questIds?: number[] | ((char: Character, chore?: Chore) => number[]); From 4e565cacd701e80dceadd05e2f8ba3ea423c5648 Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 22:28:59 -0700 Subject: [PATCH 10/11] sumChores looks a bit silly on emissaries tasks --- apps/frontend/data/tasks/06-legion/emissaryQuests.ts | 1 - apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/frontend/data/tasks/06-legion/emissaryQuests.ts b/apps/frontend/data/tasks/06-legion/emissaryQuests.ts index 5f867a688..4e14b874c 100644 --- a/apps/frontend/data/tasks/06-legion/emissaryQuests.ts +++ b/apps/frontend/data/tasks/06-legion/emissaryQuests.ts @@ -7,7 +7,6 @@ export const legionEmissaryQuests: Task = { name: '[Legion] Emissaries', shortName: 'Leg', showSeparate: true, - sumChores: true, chores: [ { key: 'courtFarondis', diff --git a/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts index 491f74646..eb022d0fe 100644 --- a/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts +++ b/apps/frontend/data/tasks/07-battle-for-azeroth/emissaryQuests.ts @@ -7,7 +7,6 @@ export const bfaEmissaryQuests: Task = { name: '[BfA] Emissaries', shortName: 'BfA', showSeparate: true, - sumChores: true, chores: [ { key: 'champions', From a944973975d688b240c077d789a6760d8e72c462 Mon Sep 17 00:00:00 2001 From: Freddie Date: Wed, 28 Oct 2026 22:38:39 -0700 Subject: [PATCH 11/11] Show expiry in decorated task header tooltip --- .../home/table/head/HomeTableHeadTasks.svelte | 10 +++++++--- .../tooltips/task/TooltipTaskHead.svelte | 14 +++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte b/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte index 316058183..7b2771517 100644 --- a/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte +++ b/apps/frontend/components/home/table/head/HomeTableHeadTasks.svelte @@ -41,12 +41,15 @@ {@const [taskName, choreName] = fullTaskName.split('|', 2)} {@const task = taskMap[taskName] || settingsState.customTaskMap[fullTaskName]} {@const chore = task.chores.find((task) => task?.key === choreName)} - {@const customExpiry = chore?.decorationFunc?.( - chore?.customExpiryFunc(chore, userState.general.characters[0], timeState.slowTime) + {@const customExpiry = chore?.customExpiryFunc?.( + chore, + userState.general.characters[0], + timeState.slowTime )} + {@const decoratorClass = chore?.decorationFunc?.(customExpiry)} setSortState(fullTaskName)} @@ -54,6 +57,7 @@ component: Tooltip, props: { chore, + expiresAt: customExpiry, fullTaskName, task, }, diff --git a/apps/frontend/components/tooltips/task/TooltipTaskHead.svelte b/apps/frontend/components/tooltips/task/TooltipTaskHead.svelte index e1d7da96d..e76a7a8c4 100644 --- a/apps/frontend/components/tooltips/task/TooltipTaskHead.svelte +++ b/apps/frontend/components/tooltips/task/TooltipTaskHead.svelte @@ -1,7 +1,11 @@