diff --git a/config/i18n.json b/config/i18n.json index 0811774e29..cbbe7b7b93 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -1095,6 +1095,7 @@ "CrucibleRank": "Ranks", "Items": "Quest Items", "Milestones": "Milestones & Challenges", + "PaleHeartPathfinder": "Pale Heart Pathfinder", "PercentPrestige": "{{pct}}% to reset", "PointsUsed": "1 point used", "PointsUsed_plural": "{{count}} points used", @@ -1108,6 +1109,7 @@ "RecordValue": "{{value}}pts", "Resets": "1 reset", "Resets_plural": "{{count}} resets", + "RitualPathfinder": "Ritual Pathfinder", "SecretTriumph": "Secret Triumph", "StatTrackers": "Stat Trackers", "TrackedTriumphs": "Tracked Triumphs", diff --git a/src/app/bungie-api/destiny2-api.ts b/src/app/bungie-api/destiny2-api.ts index 6ebf36a4be..1c467cf32c 100644 --- a/src/app/bungie-api/destiny2-api.ts +++ b/src/app/bungie-api/destiny2-api.ts @@ -98,14 +98,12 @@ export function getStores(platform: DestinyAccount): Promise -(powerBonus ?? -1)); + /** * The list of Milestones for a character. Milestones are different from pursuits and * represent challenges, story prompts, and other stuff you can do not represented by Pursuits. @@ -59,8 +61,6 @@ export default function Milestones({ } }); - const sortPowerBonus = compareBy((powerBonus: number | undefined) => -(powerBonus ?? -1)); - return ( <> {characterProgressions && ( @@ -81,7 +81,7 @@ export default function Milestones({ )} {[...milestonesByPower.keys()].sort(sortPowerBonus).map((powerBonus) => ( -
+

{powerBonus === undefined ? t('Progress.PowerBonusHeaderUndefined') @@ -117,9 +117,9 @@ function milestonesForProfile( const filteredMilestones = allMilestones.filter( (milestone) => - !milestone.availableQuests && - !milestone.activities && - (!milestone.vendors || milestone.rewards) && + !milestone.availableQuests?.length && + !milestone.activities?.length && + (!milestone.vendors?.length || Boolean(milestone.rewards?.length)) && defs.Milestone.get(milestone.milestoneHash), ); @@ -144,8 +144,8 @@ function milestonesForCharacter( return ( def && (def.showInExplorer || def.showInMilestones) && - (milestone.activities || - !milestone.availableQuests || + (Boolean(milestone.activities?.length) || + !milestone.availableQuests?.length || milestone.availableQuests.every( (q) => q.status.stepObjectives.length > 0 && diff --git a/src/app/progress/Pathfinder.m.scss b/src/app/progress/Pathfinder.m.scss new file mode 100644 index 0000000000..e60570a9e4 --- /dev/null +++ b/src/app/progress/Pathfinder.m.scss @@ -0,0 +1,101 @@ +@use '../variables.scss' as *; + +.pathfinderTree { + display: flex; + flex-direction: row; + gap: 16px; + margin: 16px 0; + + @include phone-portrait { + flex-direction: column; + margin: 16px 10px; + gap: 12px; + } +} + +.pathfinderRow { + composes: flexColumn from '../dim-ui/common.m.scss'; + box-sizing: border-box; + gap: 8px; + flex: 1; + justify-content: center; + max-width: 250px; + min-width: 120px; + --item-size: 35px; + + :global(.milestone-quest) { + align-items: center; + background: rgba(255, 255, 255, 0.05); + padding: 8px; + width: 100%; + } + + @media (min-width: 541px) { + &:nth-child(n + 2) button::before { + position: absolute; + display: inline; + content: '⧽'; + font-size: 28px; + margin-left: -23px; + } + } + + @media (max-width: 1270px) and (min-width: 541px) { + min-width: 60px; + + :global(.milestone-quest) { + align-items: flex-start; + flex-direction: column; + } + :global(.milestone-icon) { + flex-direction: row !important; + --item-size: 25px; + max-width: fit-content !important; + margin-top: 6px; + + & > div { + margin-right: 6px; + } + } + :global(.milestone-info) { + flex-direction: row !important; + width: auto !important; + } + :global(.milestone-name) { + font-size: 12px !important; + } + :global(.milestone-description) { + display: none; + } + &:not(:first-child) button::before { + transform: translateY(-8%); + } + } + + @include phone-portrait { + flex-direction: row; + gap: 14px; + width: auto; + max-width: unset; + + > * { + flex: 0; + width: calc(var(--item-size) + 16px) !important; + min-width: calc(var(--item-size) + 16px) !important; + } + :global(.milestone-icon) { + margin: 0; + } + :global(.milestone-info) { + display: none !important; + } + } +} + +.completed { + :global(.milestone-icon) img { + border-color: $xp; + border-width: 2px; + padding: 3px; + } +} diff --git a/src/app/progress/Pathfinder.m.scss.d.ts b/src/app/progress/Pathfinder.m.scss.d.ts new file mode 100644 index 0000000000..a065702494 --- /dev/null +++ b/src/app/progress/Pathfinder.m.scss.d.ts @@ -0,0 +1,9 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'completed': string; + 'pathfinderRow': string; + 'pathfinderTree': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/src/app/progress/Pathfinder.tsx b/src/app/progress/Pathfinder.tsx new file mode 100644 index 0000000000..fd67da54b2 --- /dev/null +++ b/src/app/progress/Pathfinder.tsx @@ -0,0 +1,82 @@ +import { trackedTriumphsSelector } from 'app/dim-api/selectors'; +import CollapsibleTitle from 'app/dim-ui/CollapsibleTitle'; +import { DimItem } from 'app/inventory/item-types'; +import { createItemContextSelector } from 'app/inventory/selectors'; +import { DimStore } from 'app/inventory/store-types'; +import { toPresentationNodeTree } from 'app/records/presentation-nodes'; +import { filterMap } from 'app/utils/collections'; +import { DestinyPresentationNodeDefinition, DestinyRecordState } from 'bungie-api-ts/destiny2'; +import { useSelector } from 'react-redux'; +import styles from './Pathfinder.m.scss'; +import Pursuit from './Pursuit'; +import { recordToPursuitItem } from './milestone-items'; + +/** + * List out all the seasonal challenges for the character, grouped out in a useful way. + */ +export default function Pathfinder({ + id, + name, + presentationNode, + store, +}: { + id: string; + name: string; + presentationNode: DestinyPresentationNodeDefinition; + store: DimStore; +}) { + const itemCreationContext = useSelector(createItemContextSelector); + const nodeTree = toPresentationNodeTree(itemCreationContext, presentationNode.hash); + + const allRecords = nodeTree?.childPresentationNodes?.[0]?.records?.toReversed() ?? []; + + const trackedRecords = useSelector(trackedTriumphsSelector); + + const acquiredRecords = new Set( + filterMap(allRecords, (r) => { + // Don't show records that have been redeemed + const state = r.recordComponent.state; + const acquired = Boolean(state & DestinyRecordState.RecordRedeemed); + return acquired ? r.recordDef.hash : undefined; + }), + ); + + const pursuits = allRecords.map((r) => + recordToPursuitItem( + r, + itemCreationContext.buckets, + store, + presentationNode.displayProperties.name, + trackedRecords.includes(r.recordDef.hash), + ), + ); + + const pursuitGroups: DimItem[][] = []; + for (let i = 6; i > 0; i--) { + pursuitGroups.push(pursuits.splice(0, i)); + } + + return ( +
+ +
+ {pursuitGroups.map((pursuits) => ( +
+ {pursuits.map((item) => ( + + ))} +
+ ))} +
+
+
+ ); +} diff --git a/src/app/progress/Progress.tsx b/src/app/progress/Progress.tsx index 4b299886ba..43383c7177 100644 --- a/src/app/progress/Progress.tsx +++ b/src/app/progress/Progress.tsx @@ -14,6 +14,7 @@ import { RAID_NODE } from 'app/search/d2-known-values'; import { querySelector, useIsPhonePortrait } from 'app/shell/selectors'; import { usePageTitle } from 'app/utils/hooks'; import { PanInfo, motion } from 'framer-motion'; +import _ from 'lodash'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import { DestinyAccount } from '../accounts/destiny-account'; @@ -21,6 +22,7 @@ import CollapsibleTitle from '../dim-ui/CollapsibleTitle'; import ErrorBoundary from '../dim-ui/ErrorBoundary'; import { Event } from './Event'; import Milestones from './Milestones'; +import Pathfinder from './Pathfinder'; import styles from './Progress.m.scss'; import Pursuits from './Pursuits'; import Raids from './Raids'; @@ -89,24 +91,28 @@ export default function Progress({ account }: { account: DestinyAccount }) { coreSettings?.seasonalChallengesPresentationNodeHash !== undefined && defs.PresentationNode.get(coreSettings.seasonalChallengesPresentationNodeHash); - const menuItems = [ + const paleHeartPathfinderNode = defs.PresentationNode.get(1062988660); + const ritualsPathfinderNode = defs.PresentationNode.get(622609416); + + const menuItems = _.compact([ { id: 'ranks', title: t('Progress.CrucibleRank') }, { id: 'trackedTriumphs', title: t('Progress.TrackedTriumphs') }, - ...(eventCard ? [{ id: 'event', title: eventCard.displayProperties.name }] : []), + eventCard && { id: 'event', title: eventCard.displayProperties.name }, { id: 'milestones', title: t('Progress.Milestones') }, - ...(seasonalChallengesPresentationNode - ? [ - { - id: 'seasonal-challenges', - title: seasonalChallengesPresentationNode.displayProperties.name, - }, - ] - : []), + paleHeartPathfinderNode && { + id: 'paleHeartPathfinder', + title: t('Progress.PaleHeartPathfinder'), + }, + ritualsPathfinderNode && { id: 'ritualPathfinder', title: t('Progress.RitualPathfinder') }, + seasonalChallengesPresentationNode && { + id: 'seasonal-challenges', + title: seasonalChallengesPresentationNode.displayProperties.name, + }, { id: 'Bounties', title: t('Progress.Bounties') }, { id: 'Quests', title: t('Progress.Quests') }, { id: 'Items', title: t('Progress.Items') }, - ...(raidNode ? [{ id: 'raids', title: raidTitle }] : []), - ]; + raidNode && { id: 'raids', title: raidTitle }, + ]); return ( @@ -174,6 +180,28 @@ export default function Progress({ account }: { account: DestinyAccount }) { + {paleHeartPathfinderNode && ( + + + + )} + + {ritualsPathfinderNode && ( + + + + )} + {seasonalChallengesPresentationNode && ( (

- {!hideDescription && ( -
- - - {item.name} - -
- -
+ +
+ + + {item.name} + +
+
- )} +
)} diff --git a/src/app/progress/PursuitItem.m.scss b/src/app/progress/PursuitItem.m.scss index bf7cde474a..fa155ba384 100644 --- a/src/app/progress/PursuitItem.m.scss +++ b/src/app/progress/PursuitItem.m.scss @@ -10,6 +10,7 @@ box-sizing: border-box; cursor: pointer; + :global(.pathfinder) &, :global(#milestones) &, :global(#seasonal-challenges) & { height: calc(var(--item-size) * 10 / 9 + 4px); @@ -41,6 +42,11 @@ outline: none; } + :global(.pathfinder) & { + border-radius: 50%; + padding: 4px; + } + :global(#milestones) &, :global(#event) &, :global(#seasonal-challenges) & { diff --git a/src/app/progress/PursuitItem.tsx b/src/app/progress/PursuitItem.tsx index 9dcfbd3ca5..b706425a3c 100644 --- a/src/app/progress/PursuitItem.tsx +++ b/src/app/progress/PursuitItem.tsx @@ -100,7 +100,7 @@ export function StackAmount({ amount, full }: { amount: number; full?: boolean } [styles.fullstack]: full, })} > - {amount} + {amount.toLocaleString()}
); } diff --git a/src/app/progress/Pursuits.tsx b/src/app/progress/Pursuits.tsx index 5100103a17..2f9c277399 100644 --- a/src/app/progress/Pursuits.tsx +++ b/src/app/progress/Pursuits.tsx @@ -77,13 +77,11 @@ export function PursuitsGroup({ defs, store, pursuits, - hideDescriptions, pursuitsInfo = pursuitsInfoFile, }: { defs: D2ManifestDefinitions; store: DimStore; pursuits: DimItem[]; - hideDescriptions?: boolean; pursuitsInfo?: { [hash: string]: { [type in DefType]?: number[] } }; }) { const [bountyFilters, setBountyFilters] = useState([]); @@ -103,7 +101,6 @@ export function PursuitsGroup({ item={item} key={item.index} searchHidden={!matchBountyFilters(defs, item, bountyFilters, pursuitsInfo)} - hideDescription={hideDescriptions} /> ))} diff --git a/src/app/records/presentation-nodes.ts b/src/app/records/presentation-nodes.ts index 7e8314e288..309780f74e 100644 --- a/src/app/records/presentation-nodes.ts +++ b/src/app/records/presentation-nodes.ts @@ -15,10 +15,12 @@ import { DestinyMetricComponent, DestinyMetricDefinition, DestinyPresentationNodeCollectibleChildEntry, + DestinyPresentationNodeComponent, DestinyPresentationNodeCraftableChildEntry, DestinyPresentationNodeDefinition, DestinyPresentationNodeMetricChildEntry, DestinyPresentationNodeRecordChildEntry, + DestinyPresentationNodeState, DestinyProfileResponse, DestinyRecordComponent, DestinyRecordDefinition, @@ -43,6 +45,7 @@ export interface DimPresentationNode extends DimPresentationNodeLeaf { * or generated with fake info. */ nodeDef: DestinyPresentationNodeDefinition | undefined; + nodeComponent: DestinyPresentationNodeComponent | undefined; /** May or may not be an actual hash */ hash: number; name: string; @@ -111,13 +114,20 @@ export function toPresentationNodeTree( return null; } + const nodeComponent = + profileResponse.profilePresentationNodes?.data?.nodes[presentationNodeDef.hash]; + + if ((nodeComponent?.state ?? 0) & DestinyPresentationNodeState.Invisible) { + return null; + } + // For titles, display the title, completion and gilding count const titleInfo = presentationNodeDef.completionRecordHash && genderHash ? getTitleInfo( presentationNodeDef.completionRecordHash, defs, - itemCreationContext.profileResponse.profileRecords.data, + profileResponse.profileRecords.data, genderHash, ) : undefined; @@ -128,6 +138,7 @@ export function toPresentationNodeTree( name: titleInfo?.title || presentationNodeDef.displayProperties.name, icon: presentationNodeDef.displayProperties.icon, titleInfo, + nodeComponent, }; if (presentationNodeDef.children.collectibles?.length) { const collectibles = toCollectibles( @@ -250,6 +261,7 @@ function buildPlugSetPresentationNode( visible: plugSetItems.length, acquired, plugs: plugEntries, + nodeComponent: undefined, }; return subnode; } diff --git a/src/locale/en.json b/src/locale/en.json index bf816bd1f5..05af5ed13e 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -1087,6 +1087,7 @@ "Milestones": "Milestones & Challenges", "NoEventChallenges": "You have completed all event challenges", "NoTrackedTriumph": "You have no tracked triumphs. Track as many as you like in DIM.", + "PaleHeartPathfinder": "Pale Heart Pathfinder", "PercentPrestige": "{{pct}}% to reset", "PointsUsed": "1 point used", "PointsUsed_plural": "{{count}} points used", @@ -1101,6 +1102,7 @@ "RecordValue": "{{value}}pts", "Resets": "1 reset", "Resets_plural": "{{count}} resets", + "RitualPathfinder": "Ritual Pathfinder", "SecretTriumph": "Secret Triumph", "StatTrackers": "Stat Trackers", "TrackedTriumphs": "Tracked Triumphs"