diff --git a/apps/frontend/components/items/ItemsItem.svelte b/apps/frontend/components/items/ItemsItem.svelte index 9e3b41c9f..edf0f8cc9 100644 --- a/apps/frontend/components/items/ItemsItem.svelte +++ b/apps/frontend/components/items/ItemsItem.svelte @@ -10,6 +10,8 @@ import { wowthingData } from '@/shared/stores/data'; import { getItemUrl } from '@/utils/get-item-url'; import type { Character, CharacterGear } from '@/types'; + import type { ItemDataItem } from '@/types/data/item'; + import type { CharacterProps } from '@/types/props'; import CraftedQualityIcon from '@/shared/components/images/CraftedQualityIcon.svelte'; import IconifyWrapper from '@/shared/components/images/IconifyWrapper.svelte'; @@ -17,12 +19,25 @@ import type { ConvertibleCategoryUpgrade } from './convertible/types'; import WowheadLink from '@/shared/components/links/WowheadLink.svelte'; - export let character: Character = undefined; - export let forceCrafted = false; - export let gear: Partial; - export let tierPieces: number[] = undefined; - export let useHighlighting = false; - export let useItemCount = false; + type Props = Partial & { + gear: Partial; + character?: Character; + forceCrafted?: boolean; + tierPieces?: number[]; + useHighlighting?: boolean; + useItemCount?: boolean; + failStateFunc?: (item: ItemDataItem) => boolean; + }; + + let { + gear, + character, + forceCrafted, + tierPieces, + useHighlighting, + useItemCount, + failStateFunc, + }: Props = $props(); function getIconName(): [string, number] { let tiers: ConvertibleCategoryUpgrade[][]; @@ -85,11 +100,11 @@ - - - {#each professions as [profession, userHas, slots] (profession)} @@ -130,7 +136,11 @@ {#each { length: profession?.slug === 'fishing' ? 1 : profession.type === 0 ? 3 : 2 }, slot} {@const gear = slots[slot]} {#if gear?.equipped} - + item.expansion !== Constants.expansion} + forceCrafted={true} + {gear} + /> {:else if !userHas} {:else} diff --git a/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte b/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte index 388eae2f8..58f6451e9 100644 --- a/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte +++ b/apps/frontend/components/tooltips/task/TooltipTaskRow.svelte @@ -5,6 +5,7 @@ import { settingsState } from '@/shared/state/settings.svelte'; import { DbResetType } from '@/shared/stores/db/enums'; import { userState } from '@/user-home/state/user'; + import { getPercentStatusClass } from '@/utils/get-percent-class'; import type { CharacterChore } from '@/user-home/state/user/types/tasks.svelte'; import IconifyWrapper from '@/shared/components/images/IconifyWrapper.svelte'; @@ -112,7 +113,7 @@ .error-text { font-size: 0.95rem; text-align: left; - width: 7rem; + max-width: 7rem; } .status-text { color: #afffff; @@ -194,6 +195,19 @@ {charTaskChore.statusTexts[0]} {/if} + {:else if chore.questCount > 1} + {@const cls = getPercentStatusClass( + ((chore.alwaysStarted + ? Math.max(1, charTaskChore.progressCurrent) + : charTaskChore.progressCurrent) / + charTaskChore.progressTotal) * + 100 + )} + + + {/if} diff --git a/apps/frontend/data/tasks/11-midnight/12-0-0.ts b/apps/frontend/data/tasks/11-midnight/12-0-0.ts index ef1feba52..210c19fdc 100644 --- a/apps/frontend/data/tasks/11-midnight/12-0-0.ts +++ b/apps/frontend/data/tasks/11-midnight/12-0-0.ts @@ -231,13 +231,22 @@ export const midChores12_0_0: Task = { 93758, // Nexus-Point Xenas ], }, + { + key: 'midWorldBossFirst', + name: 'World Boss (First)', + icon: iconLibrary.emojiZzz, + minimumLevel: 90, + accountWide: true, + questReset: DbResetType.Weekly, + questIds: [ + 92127, // Tracking quest? + ], + }, { key: 'midWorldBoss', name: 'World Boss', icon: iconLibrary.emojiZzz, minimumLevel: 90, - accountWide: true, - showQuestName: true, questReset: DbResetType.Weekly, questIds: [ // TODO from AreaPOI.db2, verify diff --git a/apps/frontend/data/tasks/11-midnight/delves.ts b/apps/frontend/data/tasks/11-midnight/delves.ts new file mode 100644 index 000000000..f25638e02 --- /dev/null +++ b/apps/frontend/data/tasks/11-midnight/delves.ts @@ -0,0 +1,41 @@ +import { iconLibrary } from '@/shared/icons'; +import { DbResetType } from '@/shared/stores/db/enums'; +import type { Task } from '@/types/tasks'; + +export const midDelves: Task = { + key: 'midDelves', + name: '[Mid] Delves', + shortName: 'Delve', + minimumLevel: 80, + showSeparate: true, + chores: [ + // { + // key: 'map', + // name: '{item:252415}', + // icon: iconLibrary.GameTreasureMap, + // minimumLevel: 80, + // alwaysStarted: true, + // questReset: DbResetType.Weekly, + // questIds: [], + // }, + // null, + { + key: 'arcaneRemnant', + name: '{item:262586}', + icon: iconLibrary.gameUnstableOrb, + minimumLevel: 80, + accountWide: true, + alwaysStarted: true, + questReset: DbResetType.Weekly, + questIds: [93784], + }, + // { + // key: 'nullaeus', + // name: "Nullaeus Invasion", + // minimumLevel: 90, + // alwaysStarted: true, + // questIds: [], // ?? + // questReset: DbResetType.Weekly, + // }, + ], +}; diff --git a/apps/frontend/data/tasks/11-midnight/index.ts b/apps/frontend/data/tasks/11-midnight/index.ts index e68cbb884..c1a276e21 100644 --- a/apps/frontend/data/tasks/11-midnight/index.ts +++ b/apps/frontend/data/tasks/11-midnight/index.ts @@ -1,5 +1,6 @@ import { midChores12_0_0 } from './12-0-0'; +import { midDelves } from './delves'; import { midPrey } from './prey'; import { midProfessions } from './professions'; -export const midTasks = [midChores12_0_0, midPrey, midProfessions]; +export const midTasks = [midChores12_0_0, midDelves, midPrey, midProfessions]; diff --git a/apps/frontend/data/tasks/11-midnight/prey.ts b/apps/frontend/data/tasks/11-midnight/prey.ts index 11aeb4e35..ca91b3b08 100644 --- a/apps/frontend/data/tasks/11-midnight/prey.ts +++ b/apps/frontend/data/tasks/11-midnight/prey.ts @@ -127,35 +127,24 @@ export const midPrey: Task = { minimumLevel: 80, showSeparate: true, chores: [ + { + key: 'preyRep', + name: 'Reputation', + icon: iconLibrary.gameHeartPlus, + accountWide: true, + alwaysStarted: true, + questCount: 4, + questIds: [95000, 95001, 95002, 95003], + questReset: DbResetType.Weekly, + }, { key: 'preyNormal', name: 'Normal', icon: iconLibrary.notoClownFace, alwaysStarted: true, + questCount: 4, questReset: DbResetType.Weekly, - subChores: [ - { - key: 'prey1', - name: 'Prey #1', - showQuestName: true, - questIds: preyFunc(normalQuestIds, 0), - }, - { - key: 'prey2', - name: 'Prey #2', - questIds: preyFunc(normalQuestIds, 1), - }, - { - key: 'prey3', - name: 'Prey #3', - questIds: preyFunc(normalQuestIds, 2), - }, - { - key: 'prey4', - name: 'Prey #4', - questIds: preyFunc(normalQuestIds, 3), - }, - ], + questIds: normalQuestIds, }, { key: 'preyHard', @@ -163,30 +152,9 @@ export const midPrey: Task = { icon: iconLibrary.notoCowboyHatFace, minimumLevel: 90, alwaysStarted: true, + questCount: 4, questReset: DbResetType.Weekly, - subChores: [ - { - key: 'prey1', - name: 'Prey #1', - showQuestName: true, - questIds: preyFunc(hardQuestIds, 0), - }, - { - key: 'prey2', - name: 'Prey #2', - questIds: preyFunc(hardQuestIds, 1), - }, - { - key: 'prey3', - name: 'Prey #3', - questIds: preyFunc(hardQuestIds, 2), - }, - { - key: 'prey4', - name: 'Prey #4', - questIds: preyFunc(hardQuestIds, 3), - }, - ], + questIds: hardQuestIds, }, { key: 'preyNightmare', @@ -194,30 +162,9 @@ export const midPrey: Task = { icon: iconLibrary.notoAngryFaceWithHorns, minimumLevel: 90, alwaysStarted: true, + questCount: 4, questReset: DbResetType.Weekly, - subChores: [ - { - key: 'prey1', - name: 'Prey #1', - showQuestName: true, - questIds: preyFunc(nightmareQuestIds, 0), - }, - { - key: 'prey2', - name: 'Prey #2', - questIds: preyFunc(nightmareQuestIds, 1), - }, - { - key: 'prey3', - name: 'Prey #3', - questIds: preyFunc(nightmareQuestIds, 2), - }, - { - key: 'prey4', - name: 'Prey #4', - questIds: preyFunc(nightmareQuestIds, 3), - }, - ], + questIds: nightmareQuestIds, }, ], }; diff --git a/apps/frontend/scss/core.scss b/apps/frontend/scss/core.scss index 04b6a5b1e..505db21df 100644 --- a/apps/frontend/scss/core.scss +++ b/apps/frontend/scss/core.scss @@ -119,6 +119,16 @@ code { color: #ffffaa; vertical-align: 1px; // padding: 0 0.2rem; + + &.status-success { + color: var(--color-success); + } + &.status-shrug { + color: var(--color-shrug); + } + &.status-fail { + color: var(--color-fail); + } } #app { diff --git a/apps/frontend/scss/variables.scss b/apps/frontend/scss/variables.scss index 67bada903..1093e1376 100644 --- a/apps/frontend/scss/variables.scss +++ b/apps/frontend/scss/variables.scss @@ -48,7 +48,7 @@ --color-fail: #ff1e00; --color-fail-background: color-mix(in oklch, var(--color-fail) 30%, #000 70%); - --color-fail-border: color-mix(in oklch, var(--color-fail) 70%, var(--border-color) 30%); + --color-fail-border: color-mix(in oklch, var(--color-fail) 80%, var(--border-color) 20%); // factions --color-alliance-border: #006aff; diff --git a/apps/frontend/shared/components/parsed-text/ParsedText.svelte b/apps/frontend/shared/components/parsed-text/ParsedText.svelte index 070cc5a55..717fbc3ed 100644 --- a/apps/frontend/shared/components/parsed-text/ParsedText.svelte +++ b/apps/frontend/shared/components/parsed-text/ParsedText.svelte @@ -219,7 +219,9 @@ html = html.replaceAll(/:([a-zA-Z0-9_-]+):/g, ''); // Square brackets => code - html = html.replaceAll(/(\[(.*?)\])/g, '$1'); + html = html.replaceAll(/\[(?:\|(.*?)\|)?(.*?)\]/g, (_, cls, text) => { + return cls ? `[${text}]` : `[${text}]`; + }); } afterUpdate(() => { diff --git a/apps/frontend/shared/icons/library.ts b/apps/frontend/shared/icons/library.ts index 1be896dcb..507e0cc07 100644 --- a/apps/frontend/shared/icons/library.ts +++ b/apps/frontend/shared/icons/library.ts @@ -48,6 +48,7 @@ export { default as gameHandBag } from '@iconify/icons-game-icons/hand-bag'; export { default as gameHanger } from '@iconify/icons-game-icons/hanger'; export { default as gameHatchet } from '@iconify/icons-game-icons/hatchet'; export { default as gameHeartNecklace } from '@iconify/icons-game-icons/heart-necklace'; +export { default as gameHeartPlus } from '~icons/game-icons/heart-plus'; export { default as gameHouse } from '@iconify/icons-game-icons/house'; export { default as gameJigsawBox } from '@iconify/icons-game-icons/jigsaw-box'; export { default as gameKnapsack } from '@iconify/icons-game-icons/knapsack'; @@ -81,6 +82,7 @@ export { default as gameTrident } from '@iconify/icons-game-icons/trident'; export { default as gameTrophy } from '@iconify/icons-game-icons/trophy'; export { default as gameTwirlyFlower } from '@iconify/icons-game-icons/twirly-flower'; export { default as gameTwoCoins } from '@iconify/icons-game-icons/two-coins'; +export { default as gameUnstableOrb } from '~icons/game-icons/unstable-orb'; export { default as gameUpgrade } from '@iconify/icons-game-icons/upgrade'; export { default as gameWarPick } from '@iconify/icons-game-icons/war-pick'; export { default as gameWizardStaff } from '@iconify/icons-game-icons/wizard-staff'; diff --git a/apps/frontend/types/tasks.ts b/apps/frontend/types/tasks.ts index aa27893af..6e3086335 100644 --- a/apps/frontend/types/tasks.ts +++ b/apps/frontend/types/tasks.ts @@ -29,6 +29,7 @@ export type Chore = { minimumLevel?: number; maximumLevel?: number; overrideNeed?: number; + questCount?: number; questIds?: number[] | ((char: Character, chore?: Chore) => number[]); questReset?: DbResetType; questResetForced?: boolean; diff --git a/apps/frontend/user-home/state/user/derived.svelte.ts b/apps/frontend/user-home/state/user/derived.svelte.ts index bf1a52be2..579a5b6e8 100644 --- a/apps/frontend/user-home/state/user/derived.svelte.ts +++ b/apps/frontend/user-home/state/user/derived.svelte.ts @@ -323,14 +323,22 @@ export class DataUserDerived { return maxReps; } - public doActiveViewTasks(character: Character, characterQuests: CharacterQuests) { + private _taskChoreName: Record = {}; + public doActiveViewTasks( + latestQuests: CharacterQuests, + character: Character, + characterQuests: CharacterQuests + ) { const ret: Record = {}; const customTaskMap = $state.snapshot(settingsState.customTaskMap) as Record; const showCompletedUntrackedChores = settingsState.activeView.showCompletedUntrackedChores; for (const fullTaskName of activeViewTasks.value) { - const [taskName, choreName] = fullTaskName.split('|', 2); + const [taskName, choreName] = (this._taskChoreName[fullTaskName] ||= fullTaskName.split( + '|', + 2 + )); const task = taskMap[taskName] || customTaskMap[taskName]; if ( !task || @@ -354,7 +362,13 @@ export class DataUserDerived { // if (!choreName && disabledChores.includes(chore.key)) { // continue; // } - const charChore = this.processTaskChore(character, characterQuests, task, chore); + const charChore = this.processTaskChore( + latestQuests, + character, + characterQuests, + task, + chore + ); if (!charChore) { continue; } @@ -446,13 +460,17 @@ export class DataUserDerived { } private processTaskChore( + latestQuests: CharacterQuests, character: Character, characterQuests: CharacterQuests, task: Task, chore: Chore, parent?: Chore ): CharacterChore { - if (!character || !characterQuests) { + if (chore.accountWide && !latestQuests) { + return null; + } + if (!chore.accountWide && (!character || !characterQuests)) { return null; } @@ -472,11 +490,18 @@ export class DataUserDerived { return null; } + const useQuests = chore.accountWide ? latestQuests : characterQuests; + const charChore = new CharacterChore(chore.key, undefined); - const charScanned = characterQuests.scannedTime; + if (chore.questCount) { + charChore.progressTotal = chore.questCount; + } + + const charScanned = useQuests.scannedTime; const choreReset = chore.questReset || parent?.questReset; const resetForced = chore.questResetForced === true || parent?.questResetForced === true; + let completedCount = 0; let questIds: number[] = []; if (chore.questIds) { questIds = @@ -505,7 +530,7 @@ export class DataUserDerived { for (const questId of questIds) { // is the quest in progress? - const questProgress = characterQuests?.progressQuestByKey?.get(`q${questId}`); + const questProgress = useQuests?.progressQuestByKey?.get(`q${questId}`); if ( questProgress && (!resetForced || @@ -527,9 +552,9 @@ export class DataUserDerived { } // is the quest completed? - if (characterQuests?.hasQuestById?.has(questId)) { + if (useQuests?.hasQuestById?.has(questId)) { if (choreReset === DbResetType.Never || expiresAt > timeState.slowTime) { - charChore.progressCurrent = 1; + charChore.progressCurrent++; charChore.quest = { expires: expiresAt?.toUnixInteger(), id: questId, @@ -538,7 +563,11 @@ export class DataUserDerived { status: QuestStatus.Completed, }; } - break; + + completedCount++; + if (!chore.questCount || completedCount >= chore.questCount) { + break; + } } } } else if (chore.subChores) { @@ -548,6 +577,7 @@ export class DataUserDerived { for (const subChore of chore.subChores) { const charSubChore = this.processTaskChore( + latestQuests, character, characterQuests, task, @@ -613,6 +643,7 @@ export class DataUserDerived { } if ( + !chore.questCount && charChore.status === QuestStatus.InProgress && charChore.quest?.objectives?.length > 0 ) { @@ -627,19 +658,29 @@ export class DataUserDerived { DateTime.fromSeconds(charChore.quest.expires) > timeState.slowTime || (chore.key.startsWith('dmf') && charChore.quest.expires === 0)) ) { - // charTask maybe? - charChore.status = charChore.quest.status; - if ( - charChore.status === QuestStatus.InProgress && - charChore.quest.objectives?.length > 0 - ) { - const lastObjective = charChore.quest.objectives.at(-1); - charChore.progressCurrent = lastObjective.have; - charChore.progressTotal = lastObjective.need; + if (chore.questCount) { + if (charChore.progressCurrent >= charChore.progressTotal) { + charChore.status = QuestStatus.Completed; + } else if (charChore.progressCurrent > 0) { + charChore.status = QuestStatus.InProgress; + } + } else { + // charTask maybe? + charChore.status = charChore.quest.status; + if ( + charChore.status === QuestStatus.InProgress && + charChore.quest.objectives?.length > 0 + ) { + const lastObjective = charChore.quest.objectives.at(-1); + charChore.progressCurrent = lastObjective.have; + charChore.progressTotal = lastObjective.need; - charChore.statusTexts = this.getObjectivesText(charChore.quest.objectives); + charChore.statusTexts = this.getObjectivesText(charChore.quest.objectives); + } } - } else if (chore.alwaysStarted && charChore.status === QuestStatus.NotStarted) { + } + + if (chore.alwaysStarted && charChore.status === QuestStatus.NotStarted) { charChore.status = QuestStatus.InProgress; } @@ -657,6 +698,8 @@ export class DataUserDerived { charChore.name ||= chore.name; + if (chore.key === 'preyRep') console.log(character.name, charChore); + return charChore; } diff --git a/apps/frontend/user-home/state/user/quests.svelte.ts b/apps/frontend/user-home/state/user/quests.svelte.ts index 0163f59d3..09df4cc64 100644 --- a/apps/frontend/user-home/state/user/quests.svelte.ts +++ b/apps/frontend/user-home/state/user/quests.svelte.ts @@ -1,3 +1,4 @@ +import sortBy from 'lodash/sortBy'; import { SvelteSet } from 'svelte/reactivity'; import { getNumberKeyedEntries } from '@/utils/get-number-keyed-entries'; @@ -46,6 +47,15 @@ export class DataUserQuests { return ret; }); + public mostRecentCharacter = $derived( + sortBy( + Array.from(this.characterById.values()).filter( + (charQuests) => !!charQuests.scannedTime + ), + (charQuests) => -charQuests.scannedTime + )[0] + ); + public progressQuestCharactersByKey = $derived.by(() => { const ret: Record = {}; diff --git a/apps/frontend/user-home/state/user/userState.svelte.ts b/apps/frontend/user-home/state/user/userState.svelte.ts index 5d4bf23c9..c2f58a1f8 100644 --- a/apps/frontend/user-home/state/user/userState.svelte.ts +++ b/apps/frontend/user-home/state/user/userState.svelte.ts @@ -81,6 +81,7 @@ class UserState { const ret: Record> = {}; for (const character of userState.general.activeCharacters) { ret[character.id] = this.derived.doActiveViewTasks( + userState.quests.mostRecentCharacter, character, userState.quests.characterById.get(character.id) ); diff --git a/apps/frontend/utils/get-percent-class.ts b/apps/frontend/utils/get-percent-class.ts index e0304fd37..83e909802 100644 --- a/apps/frontend/utils/get-percent-class.ts +++ b/apps/frontend/utils/get-percent-class.ts @@ -24,3 +24,17 @@ export default function getPercentClass(percent: number | UserCount): string { return 'quality1'; } } + +export function getPercentStatusClass(percent: number): string { + if (percent === undefined) { + return 'status-fail'; + } + + if (percent >= 100) { + return 'status-success'; + } else if (percent > 0) { + return 'status-shrug'; + } else { + return 'status-fail'; + } +}